Linux入门——11 线程

线程的概念,线程的控制,线程的同步和互斥,队列结构,线程池,锁

1.预备知识

1.1可重入函数

1.1.1链表的头插

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
  • 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

main执行流和信号捕捉方法执行流同时发生,就会出现两个执行流,

结论:1.一般而言,我们认为:main执行流和信号捕捉执行流是两个执行流

2.如果在main中,和handler中,该函数被重复进入,出问题,该函数被称为-----不可重入函数

3.如果在main中,和handler中,该函数被重复进入,没有出问题,该函数被称为-----可重入函数

***************我们目前用到的接口都是不可重入的。

函数可不可以重入是特性,是一个中性词!

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

1.2 volatile(保持内存可见性)


#include <stdio.h>
#include <signal.h>

int quit = 0;

void handler(int signo)
{
    printf("%d号信号正在被捕捉\n",signo);
    printf("quit: %d",quit);
    quit = 1;
    printf(" -> %d\n",quit);
}

int main(int argc, char const *argv[])
{
    signal(2,handler);
    while(!quit);
    printf("我是正常退出的\n");
    return 0;
}

cpu主要做三件事情,取指令,分析指令,执行指令

正常情况下在main执行流中,更改quit的值是在内存中改的,但是进行优化后,quit被放到寄存器中,这时如果执行信号捕捉动作流的时候,改的还是内存的数据,但寄存器中的值还是0,没有被改,所以main执行流还是使用寄存器中的quit.

所以为了解决这类问题,我们要使用关键字volatile

让每一次读取quit的值,不从寄存器中读取,而是从内存中读取。

 2.什么是线程

在创建进程的时候会有虚拟内存,虚拟内存里面决定了进程能够看到的资源(代码区,堆区,栈区。。。),我们通过地址空间和页表就能访问代码所需要的资源,

一个进程是可以把自己的划分出一部分,让另一样执行流去执行,如fork(),让父子进程分别执行不同的代码块。也可以发生写实拷贝。

先在我们让再次创建的进程不再有独立的地址空间,而是共享父进程的地址空间,就相当于一个房间,有5,6个人一起通过窗户看,每个人都可以通过同一个窗户看,让每一个进程访问我代码中的一部分,访问一部分资源

类似这种,只创建pcb(struct_task)不分配地址空间的,从父进程中分配资源的方法叫做线程。

因为我们可以通过虚拟地址空间+页表方式对进程进行资源划分,单个“进程”执行力度,一定要比之前的进程要细

  • 站在CPU的角度,会如何看待一个一个的PCB(task_struct)呢?

CPU不会管你有没有虚拟地址空间。它只认识task_struct,只会对每一个PCB进行计算

  • 如果OS要专门创建设计“线程”概念,OS要不要进程管理?

可能需要,如果有,如何管理呢?

先描述在组织

一定要对线程设计专门的数据结构对象(TCB),常见的Windows系统就是专门这样做的

线程创建的本质就是为了被执行。被调度(id,状态,优先级,上下文,栈。。。。);

这时会发现,线程和进程有很多的地方是重叠的,

所以我们的Linux工程师,我们不想给Linux系统专门设置线程的数据结构,我们直接用PCB来表示Linux下的“线程”。

Linux下的线程,就是在OS内创建pcb,然后指向父线程的地址空间,通过页表给线程分配一些资源

结论:线程在进程内运行,线程在进程地址空间内运行!拥有进程的一部分资源。

进程的概念:内核视角下,承担分配系统资源的基本实体(创建进程时候,申请一个地址空间,创建一个pcb和一堆的页表和加载到物理内存的代码和数据,所有这些消耗的资源,我们叫做进程,以前讲的进程内部只有一个执行流)

线程的概念:CPU调度的基本单位!

以前是一个pcb和地址空间页表内存中的代码和数据,叫做进程

现在是一堆的PCB和地址空间页表内存中的代码和数据,叫做进程

以前讲的进程内部只有一个执行流,今天讲的是一个进程内部可以有多个执行流,

站在CPU角度,以前我们讲的是CPU调度的是一个进程,今天,CPU调度的是进程中的一个分支(执行流)

今天我们喂给CPU的task_struct

  • Linux内核中有没有真正意义的线程呢?

1.严格意义上是没有的,Linux利用进程PCB模拟线程的,是一种完全属于自己的一套方案。

2.站在CPU视角,每一个PCB,都可以称为轻量级进程

3.Linux线程是CPU调度的基本单位。而进程是承担分配资源的基本单位

4.进程是用来整体申请资源,线程是伸手向进程要资源。

5.Linux中没有真正意义上的线程。没有线程之名但有线程之实

6.好处是:可以服用PCB的调度算法,并不用维护线程与进程的关系,不用做数据结构之间的耦合,让编码上更简单,维护成本低------可靠高效

7.缺点是:OS、程序员只认线程,Linux无法直接提供线程的系统调用接口,而只能给我们提供轻量级进程的接口!(去银行打不了饭)

后续如果资源不足,线程需要资源,OS还是会给的,只是本质上是进程在要。

2.1pthread_create(线程创建库函数)

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

参数:

pthread_t *thread无符号整数的线程id

const pthread_attr_t *attr ,不要管,全设为NULL

void *(*start_routine) (void *) 函数指针,让创建的线程执行这个函数(回调函数)

void *arg 回调函数,在执行时使用的参数

返回值:

成功返回0,失败返回错误码

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <unistd.h>
#include <cstdio>

using namespace std;


//新线程
void* thread_routine(void* args)
{
    const char* name = (const char*)args;
    while(true)
    {
        cout<<"我是新线程,我正在运行,name:" << name << std::endl;
        sleep(1);
    }
    
}

int main(int argc, char const *argv[])
{
    pthread_t tid;  //tid地址
    int n = pthread_create(&tid,nullptr,thread_routine,(void*)"thread_on");
    assert(0 == n);
    (void)n;


    //主线程
    while (true)
    {
        char tidbuffer[64];  
        snprintf(tidbuffer,sizeof(tidbuffer),"0x%x",(unsigned int)tid);
        cout<<"我是主线程,我正在运行,我创建出来的线程tid:"<< tidbuffer << endl;
        sleep(1);
    }
    


    return 0;
}

需要使用命令ps -aL查看轻量级进程

有两个执行流,且pid是一样的。说明这两个是属于同一个进程的,但是LWP是不一样的,LWP(light weight process)轻量级进程ID

但PID和LWP是一样的轻量级线程为主线程。

CPU调度是以LWP为标识符表示特定的一个执行流的。

以前我们理解的是PID,是因为用的都是单线程,PID==LWP

pthread_create函数得到的tid是一个地址,与系统看到的LWP是并不一样的。

2.1线程一旦被创建,几乎所有资源都是被所有线程共享的

 

所有的线程都能调用定义的函数

线程之间的数据的很方便共享

但是不是所有的数据都共享,只是大部分的数据是共享的。

线程也一定有自己的私有内部属性

什么资源是线程私有的?

1.PCB属性私有,

2.要有一定的私有上下文结构

3.每一个线程都要有自己的独立的栈结构

3.错误号

5.优先级

与进程之间的切换相比,进程之间的切换需要操作系统需要OS做的工作少很多。

1. 进程:切换页表 && PCB &&上下文 && 虚拟地址空间

2. 线程:切换PCB && 上下文数据

3.线程切换cache不用太更新,但是进程切换,就要全部更新

CPU中除了有寄存器还有cache(硬件级缓存),对数据的保存的功能,CPU在访问数据的时候,可以不访问内存,直接访问cache.如果cache没有命中,cache从内存中去读取,再让CPU来读取。

一个进程它的内部已经缓存了许多热点数据!

如果是进程,这里面的数据都要进行切换,而如果是线程的话,这里面的数据就不用被切换。

线程的缺点:

性能的损失:线程数和核数最好是一样的,比如CPU是单核的,现在是3个线程,线程也要进行切换。

CPU的多核:CPU中有运算器和控制器,多核可以理解为CPU中存在多个运算器

多CPU就是多个独立的CPU

健壮性降低:一个线程出问题,可能会影响多个线程

缺乏访问控制:全局变量的访问控制

编程调试困难

线程的优点:

创建新线程的代价要比创建有一个新进程的代价小的多

线程之间的切换不进程之间的切换,系统做的工作要少很多

线程占 的资源要少的多

能够充分使用处理器完成并发的工作

在等待慢速IO过程中,程序可执行其他操作

计算密集型应用能够在多处理器上运行

io密集型应用,为了提高性能,将io操作重叠

线程共享的资源

除了pcb,内存地址空间,页表,

还有文件描述符表,每种信号的处理方式是共享的,当前工作目录,用户id和组id

线程私有资源

线程ID

寄存器(上下文)

ernno

信号屏蔽字

调度优先级

3.线程的健壮性问题

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>

using namespace std;

void* start_routine(void* argv);


int main(int argc, char const *argv[])
{
    pthread_t id;
    pthread_create(&id,nullptr,start_routine,(void*)"start_routine");
    while (true)
    {
        cout << "main thread running" <<endl;
        sleep(1);
    }
    
    return 0;
}

void* start_routine(void* argv)
{  
    string name = static_cast<const char*>(argv);   //安全的强制类型转换并检查
    while (true)
    {
        cout << "new thread:" << name <<endl;
        sleep(1);
    }
    int *p =nullptr;
    // p = nullptr;   //p本来就是空指针
    *p = 0; //这个会报错,直接对nullptr解引用是不行的,本质是向0号地址内写0
    
}

这里 *p = 0,野指针错误。会影响整个进程

一个线程如果出现问题,会影响其他线程,这可以说是健壮性或鲁棒性差

为什么呢?

之前讲的信号是进程信号,发送给整个进程的,所有线程的pid都是想等的,所以OS向所有的同一个pid号进程发送信号会导致整个所有线程全部结束

一个进程包括地址空间,页表和内存中对应的代码和结构,多个或一个执行流

一个线程是进程的一部分,如果线程出了问题,就相当于进程出现了问题。 进程要被释放,所有依附这个进程的线程都会被释放

4.线程和进程的关系

5.clone线程创建调用库,但依旧是要系统提供接口

fork底层也是调用这个接口,

clone,生成进程或者轻量级进程

int clone(int (*fn)(void *), void *child_stack,int flags, void arg, .../ pid_t *ptid, void *newtls, pid_t *ctid */ );

参数

        int (*fn)(void *) //新执行流要执行的代码

        void *child_stack //子栈

vfork也可以创建子线程,不过与fork不同的是,创建出来的线程与父线程,共享地址空间,也就是轻量级进程

6.线程控制

6.1线程库

#POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
  • 要使用这些函数库,要通过引入头文
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

6.2 pthread_create创建线程

功能:创建一个新的线程

原型

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);

参数

        thread:返回线程ID

        attr:设置线程的属性,attr为NULL表示使用默认属性

        start_routine:是个函数地址,线程启动后要执行的函数

        arg:传给线程启动函数的参数

返回值:

成功返回0;失败返回错误码

错误检查:

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小

6.3线程的终止

6.3.1线程函数结束,return的时候,线程就终止了

6.3.2pthread_exit(void *retval);线程终止函数

以前我们学到exit(0);函数不能用于终止线程,会让整个进程直接终止。

---------》任何一个执行流调用exit都会让整个进程结束任务

void pthread_exit(void *retval);

参数:void *retval

这个参数和如何获得这个参数,通过线程等待之后解决

 6.3.3pthread_cancel线程取消

线程是可以被取消的,线程被取消,前提是线程已经跑起来了。

int pthread_cancel(pthread_t thread);

参数:线程号;

线程如果是被取消的,它的退出码就是-1,本质是一个宏PTHREAD_CANCELED;

线程必须有阻塞点(sleep)才能被取消。

如果没有取消点,可以手动添加

void pthread_testcancel(void);

有的线程可以取消,有的线程不可以取消,可以使用,但是如果不让取消,一定要保证状态修改这句代码已经执行,否者会直接取消。

int pthread_setcancelstate(int state, int *oldstate);

state:

PTHREAD_CANCEL_ENABLE //可以被取消

PTHREAD_CANCEL_DISABLE //不可以被取消

例如前5秒不能被取消,后面可以被取消

还可以设置取消类型

int pthread_setcanceltype(int type, int *oldtype);

PTHREAD_CANCEL_DEFERRED 等到取消点才取消(默认)

PTHREAD_CANCEL_ASYNCHRONOUS 目标线程会立即取消

6.2.4pthread_clean_push线程的清理

如果 线程在取消前申请了内存没有释放,就会浪费资源,这时候,就需要线程清理

void pthread_cleanup_push(void (*routine) (void *), void *arg)

void pthread_cleanup_pop(int execute)

这两个函数必须成对使用

没有成对使用报的错

正确使用

pthread_cleanup_pop的参数如果是0,就不会在执行pthread_cleanup_push里面的回调函数了

如果pthread_cleanup_pop的参数如果是非0,就会直接去执行pthread_cleanup_push里面的回调函数

这个pthread_cleanup_push里面的回调函数被执行的条件

1. 被pthraead_cancel取消掉

2.执行pthread_exit

3.非0参数执行pthread_cleanup_pop

线程中的return可以直接结束线程,但是不能触发pthread_cleanup_push里面的回调函数

6.3.5pthread_self(),获取子线程tid

 7.线程的等待

线程也是需要等待的,如果不等待会发生什么问题呢?

如果不等待,也会照成类似僵尸进程的问题-----内存泄露。

等待的目的:

  • 获取回收新线程的退出信息------》可以不关心,但是不能没有
  • 回收新线程对应的PCB等内核资源,防止内存泄露-------暂时无法查看!

等待的方法:int pthread_join(pthread_t thread, void **retval);

7.1pthread_join()线程等待函数

int pthread_join(pthread_t thread, void **retval);

参数:

        pthread_t thread //线程id

        void **retval //void pthread_exit(void *retval);

返回值:

成功返回0,失败返回错误码

//多线程的等待
    for(auto &iter : threads)
    {
        pthread_join(iter->tid,nullptr);
    }

7.2线程的返回值问题

无论是pthread_exit还是pthread_join函数,都让我们传入参数返回值

针对int pthread_join(pthread_t thread, void **retval);中的void ** retval是输出型参数,用来获取线程函数结束时,返回的退出结果!!!

线程返回值结果是void*,要想将其输出就要使用void**

类型是什么:

我身上有100,我身上有多少钱?100元,100美元,100分、、、、、

你并不知道

现在返回值如果是return (void*)106;

代表的就是返回的是地址,只是这个地址里面写的是106

在我们自己的代码空间中,定义了一个变量,void*ret;而(void*)106是指针。

我们的线程退出的时候,会将退出结果保存到pthread库中。

pthread_joint的本质是从库中调取指定线程的退出信息

现在如何获取这个退出信息到我们定义的变量中呢?

在你的空间中定义一个指针变量。由于是个变量,可以将指针变量的地址传进去,

&ret

*(&ret) 就等于库中的这个变量,

把库中的变量(void*)ret拷贝到&ret中,再解引用,就是这个数本身,就相当于直接将这个数拷贝到ret中

 7.3线程退出的信号

线程出异常,整个进程都会退出。

//ptread_join默认就会调用成功,不考虑异常问题,异常问题是你进程考虑的问题。

7.4分离线程。如果线程不进行等待

线程不存在非阻塞等待,要么等,要么不等。

分离线程

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

线程可以自己分离自己,也可以由父进程进行分离

7.4.1pthread_detach线程分离函数

 

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstdio>

std::string changeId(const pthread_t &threadid)
{
    char tid[128];
    snprintf(tid,sizeof(tid),"ox%x",(unsigned int)threadid);  //自己获取自己的线程id
    return tid;
}


void* start_routine(void* args)
{
    std::string thread_name = static_cast<const char*>(args);
    pthread_detach(pthread_self()); //自己把自己设置为分离状态
    while(true)
    {
        
        std::cout << "name:" << thread_name << "running...new thread id:"<< changeId(pthread_self()) << std::endl;
        sleep(1);
    }
}




int main(int argc, char const *argv[])
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread_1");
    std::string main_id = changeId(pthread_self());
    std::cout <<  "main running...main thread id:"<< main_id << "   new thread id:"<< changeId(tid) << std::endl;

    pthread_join(tid,nullptr);
    return 0;
}

一个线程默认是jointable的,如果设置了分离状态就不能再分离了。

7.4.2新线程自己分离自己

 pthread_self首先要获得自己的线程id

谁调用这个函数,就返回这个线程的id

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>

std::string changeId(const pthread_t &threadid)
{
    char tid[128];
    snprintf(tid,sizeof(tid),"ox%x",(unsigned int)threadid);  //自己获取自己的线程id
    return tid;
}


void* start_routine(void* args)
{
    std::string thread_name = static_cast<const char*>(args);
    pthread_detach(pthread_self()); //自己把自己设置为分离状态
    int cnt = 5;
    while(cnt--)
    {
        
        std::cout << "name:" << thread_name << "running...new thread id:"<< changeId(pthread_self()) << std::endl;
        sleep(1);
    }
}




int main(int argc, char const *argv[])
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread_1");
    std::string main_id = changeId(pthread_self());
    std::cout <<  "main running...main thread id:"<< main_id << "   new thread id:"<< changeId(tid) << std::endl;

    int n = pthread_join(tid,nullptr);
    std::cout<<"result " << n << ":" << strerror(n) << std::endl;
    return 0;
}

std::cout

无论线程是否被分离,int n = pthread_join(tid,nullptr);中的n都是0,success,为什么呢?

新线程和主线程,创建号后谁先运行?

不确定,如果新线程还没执行pthread_detach,主线程直接join,就直接进入阻塞等待了。不管你后面有没有分离

这是有问题的。

一个线程被join的时候,一定要保证已经被分离了。可以在之前先sleep一下。

这种做法,不太合理,还是推荐由主线程直接将新线程分离。

 7.4.3主线程分离新线程

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>

std::string changeId(const pthread_t &threadid)
{
    char tid[128];
    snprintf(tid,sizeof(tid),"ox%x",(unsigned int)threadid);  //自己获取自己的线程id
    return tid;
}


void* start_routine(void* args)
{
    std::string thread_name = static_cast<const char*>(args);
    //pthread_detach(pthread_self()); //自己把自己设置为分离状态
    int cnt = 5;
    while(cnt--)
    {
        
        std::cout << "name:" << thread_name << "running...new thread id:"<< changeId(pthread_self()) << std::endl;
        sleep(1);
    }
}


int main(int argc, char const *argv[])
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread_1");
    pthread_detach(pthread_self()); //主线程在创建好新线程的时候,直接将其设置为分离状态
    std::string main_id = changeId(pthread_self());
    std::cout <<  "main running...main thread id:"<< main_id << "   new thread id:"<< changeId(tid) << std::endl;
    sleep(2);
    int n = pthread_join(tid,nullptr);
    std::cout<<"result " << n << ":" << strerror(n) << std::endl;
    return 0;
}

7.4.4pthread_attr_t attr; /*通过线程属性来设置游离态(分离态)*/

设置线程属性为分离

pthread_attr_t attr;

pthread_attr_init(&attr);

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

pthread_create(&tid,&attr,func,NULL);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值