Linux线程

目录

页表

线程概念

pthread_create()

线程资源共享

线程的优缺点

优点

缺点

线程异常

线程控制

线程终止

pthread_exit()

pthread_cancel()

线程等待

线程分离

线程ID

C++线程库

封装线程库


在介绍线程前,先进一步了解一些页表知识点

页表

地址空间是进程能看到的资源窗口,页表决定进程真正拥有资源的情况。

合理的对地址空间+页表进行划分,就可以对一个进程的所有资源进行分类

在线程之前,页表的理解就是一个条目对应着虚拟地址的物理地址,还有各种权限。按照32位系统来算,一个进程地址空间4GB,就可以存2^32个地址,那么页表就要有2^32个条目,假设一个条目6字节,一个页表就需要24GB的空间,这非常不现实!!

因此,实际的页表存储不是这样的单级页表

在Linux中,物理内存也需要被管理,物理内存的最小单位是Page Frame(页帧/页框) ,占用4字节,根据先描述再组 织,通过struct page mem[]数组的方式管理物理内存,物理内存中有多少页就有多少个struct page 。每个struct page用于存对应Page Frame的元数据(例如这张页被谁用了,内容被修改过吗,这张页是用户态还是内核态等等)

如果一个虚拟地址4字节,整体作映射的话,2^32个地址就需要20多GB的空间,因此需要把虚拟地址拆开,一个32位的虚拟地址是按照10,10,12 的方式划分的,前两部分都分别由一个页表结构进行管理

前10位 地址由页目录进行查找,那么一个页目录中就需要有2^10个条目

当在页目录中找到对应条目后,每个条目又有一个对应的页表项,用于查找后10位 ,因此每一个页表项也有2^10个条目

当在页表项中找到对应条目后,会直接到物理内存的某一页(page Frame)

最后12位 能表示2^12 = 4KB,而物理内存的Page Frame 也是4KB空间,因此它们之间可以一一对应

因此页表项会直接跳转到物理内存的页帧起始地址,只需移动最后12位的偏移量就能找到物理地址

并且页表项不一定会被全部使用,若页目录中只有3项有值,那么就只会创建3个页表项,以节省内存空间

就算页表项全部被使用,也只有20个比特位,相比之前说的32个比特位会节省很多空间

线程概念

在之前的理解中,对于进程的定义是内核数据结构+进程地址空间,我们可以通过fork创建子进程,子进程也会创建自己的数据结构和进程地址空间,此时子进程和父进程的物理地址一致,但只要写时拷贝就会更改

通过fork创建的子进程,就是一个代码里有两个执行流,一个执行流执行属于父进程部分的代码,一个执行流执行属于子进程部分的代码

而对于线程,现在就可以简单的理解为进程内的一个执行流

对于进程地址空间,它决定了一个进程能够看到的"资源",如果将代码区分成几部分,即页表也划分几部分,再创建几个"进程",只创建新的PCB,都指向原先已有进程的进程地址空间、页表

每个新创建出的PCB都执行一部分代码 ,这种和原先进程共享同一份进程地址空间的PCB,就叫线程

若LinuxOS有线程的概念,那就离不开对线程的管理,也就是先描述再组织 ,就要设计线程的数据结构TCB(thread contral blcok),既然创建出了线程,就要被执行,被调度,就需要有ID,状态,优先级,上下文,栈等等属性。

到这里会发现线程和进程有很多地方是重叠 的,因此在Linux中,没有专门的"线程"概念,而是直接复用PCB ,用PCB表示Linux内部的线程(Windows有专门的一套线程数据结构),线程在进程的地址空间内运行,拥有该进程的一部分资源

现在线程也可以理解为内核数据结构+对应的代码数据 ,因此就需要重新认识进程:++在内核视角中,进程是承担分配系统资源的基本实体 ,也就是说只有当它拥有系统分配的PCB和进程地址空时,才是进程。(对于线程,真正属于它自己的只有PCB)++

线程是CPU调度的基本单位,在CPU中并不区分这个PCB到底是线程还是进程,都遵循时间片调度的规则。只不过线程的PCB跟进程的PCB所比较,线程的PCB分量更轻,即轻量级进程

对于之前的进程,只有一个PCB,因此可以理解为单线程(每个进程至少有一个线程),而本篇就会介绍多线程应用

总结:

  1. Linux内核中没有真正意义的线程,而是通过进程PCB模拟线程轻量级进程为LWP
  2. 站在CPU的角度,每一个PCB,都可以称之为轻量级进程
  3. Linux中线程是CPU调度的基本单位 ,而进程是承担分配系统资源的基本单位
  4. 进程用来整体申请资源,线程用来伸手向进程要资源

这样做的好处是可以复用进程PCB的数据结构,低耦合高内聚,维护成本大大降低,可靠高效

坏处就是OS和程序员只认线程,不认轻量级进程,因为这只是Linux内部独创的一个概念。Linux无法直接提供创建线程的系统调用接口,只能提供创建轻量级进程的接口,因此中间还需要一个用于连接用户和Linux的接口

pthread_create()

在程序中创建线程,就需要用到 pthread_create()

  • thread是输出型参数,用于保存线程的ID
  • attr为线程属性,这里直接设为nullptr即可
  • start routine是一个函数指针,用于指定线程执行的任务
  • arg参数会传给start routine的void*参数
cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
using namespace std;

void* thread(void* args)//线程要执行的任务
{
    while(true)
    {
       cout << "我是一个线程:" << (char*)args << "\n";
       sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid,nullptr,thread,(void*)("无敌线程"));
    assert(ret == 0);//线程正常被创建时返回0
    while(true)
    {
        cout << "我是一个进程\n";
        sleep(1);
    }
    return 0;
}

但是当编译时会报错,提示找不到pthread_create函数

这是因为pthread_create是库函数提供的线程接口,Linux中本身没有线程概念,因此OS找不到该库。要想让OS能找到,就要在编译时用-l指定Linux的pthread库(只要在Linux中用线程,就必须用到该库)

cpp 复制代码
mysign:mysign.cpp
	g++ -o $@ $^ -std=c++11 -g -lpthread

现在再编译就可以通过了,用ldd查看一下动态库,就会看到pthread动态库

当然,pthread除了有动态库外,还有静态库

运行时会感觉和父子进程运行的效果差不多

但如果ps axj命令查一下,就会发现这是一个进程的两个执行流,而不是两个进程

kill杀死该进程时,两个线程都会终止

当用ps -aL查询时(a为all,L为线程),就会发现该进程有两个线程

LWP (light weight process)就是轻量级进程ID 。CPU调度时,就是以LWP标识唯一性 ,即以LWP为基本单位 ,LWP又称为TID

pthread_create()中有一个输出型参数thread,用于保存线程ID,如果把该ID打印出来,会发现和原先想的不太一样,并不是LWP/TID,而是不明所以的长字符串,这个到后面会详细介绍

线程资源共享

在线程被创建后,该进程的大部分资源都是被共享的,例如代码区(这里用函数举例)、数据段(这里用全局变量举例)、堆区(这里用动态开辟内存举例)等等(除此之外,文件描述符表、每种信号的处理方法、当前工作目录、用户id和组id也是公有的)

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <string>
#include <assert.h>
#include <unistd.h>
using namespace std;

const char* func()
{
    return "【函数返回】"; 
}

int global_val = 114514;//全局变量

void* thread(void* args)//线程要执行的任务
{
    while(true)
    {
       printf("我是一个线程:%s:%d:%d\n",func(),global_val,*((int*)args));
       sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int* sss = new int(10); //动态开辟
    int ret = pthread_create(&tid,nullptr,thread,(void*)sss);
    assert(ret == 0);//线程正常被创建时返回0
    while(true)
    {
        printf("我是一个进程:%s:%d:%d\n",func(),global_val,*sss);
        sleep(1);
    }
    return 0;
}

在以前的多进程通信中,所有技术都是在解决要想让不同进程看到同一份资源,而在多线程中根本不需要,它们天然支持

线程除了有共享资源,也需要有私有资源。因为线程是CPU调度的基本单位,所以线程需要有独立的ID,优先级,状态等等 ,也就是线程的PCB属性必须是私有

当一个线程的时间片被用完后,需要被切换,此时就需要把独属于该线程的上下文数据保存起来,因此线程有私有的上下文结构

对于上面程序,不管是主线程(main函数)还是新线程(thread函数),在运行时都会有局部变量,这里的局部变量是私有的,也就是说,线程有独立的栈区(除此之外,++线程ID、信号屏蔽字、调度优先级也是私有的++)

一个进程地址空间只有一个栈区,是怎么保证每个线程都有自己独立的栈区呢?

不管是pthread_create接口还是之前的fork接口,都要新建PCB,只是进程地址空间共享不共享的问题,它们底层都是通过**clone()**系统调用实现的

若是创建线程,参数fn就是线程要执行的函数,参数child_stack就是子栈

vfork接口也是创建子进程,但它和父进程共享同一块地址空间 ,也就是轻量级进程的概念,该接口也是通过clone系统调用实现的(一般不用vfork)

线程的优缺点

优点

创建一个新线程的代价比创建一个新进程小的多。创建新线程只需要创建一个新的PCB,而创建进程需要创建PCB,进程地址空间,页表等等操作

与进程切换相比,线程切换需要OS做的工作要少得多。

进程切换需要++切换页表&&切换PCB&&切换上下&&切换进程地址空间++ ,而线程切换只需要++切换PCB&&切换上下文++。

CPU访问数据时会优先从Cache中查找 ,若未命中则从内存获取,并将该数据所在的数据块(缓存行)整体加载到Cache中 ,这是基于局部性原理的优化机制,能显著提高数据访问效率。在一个进程已经运行了一段时间后,Cache中已经缓存了很多热点数据,如果是线程切换,Cache中的部分数据会被保留 ,因为该数据很有可能被其他线程再次访问。但如果是进程切换,在切换后,Cache的数据全部无效化,就需要新进程重新从内存加载所需数据到Cache中。即线程切换时Cache部分更新,但进程切换时Cache需要全部更新

线程占用的资源比进程少得多,因为线程资源都是进程给的

计算密集型应用,可以将计算分解到多个线程中实现(多核处理器系统的前提下);I/O密集型应用可以多个线程等待不同的I/O操作

计算密集型应用就是使用CPU多的,例如加密/解密,算法;I/O密集型应用就是访问外设多的,例如访问磁盘/显示器/网络...

缺点

线程不是越多越好,当线程数超过CPU核心处理能力时,就会有性能损失 (对于计算密集型任务,线程数 ≈ CPU核心数

多线程程序的健壮性较低,若其中一个线程出错,会终止整个进程,即线程之间缺乏保护

线程缺乏访问控制,有可能会出现多个线程同时访问一个全局变量进而对整个进程造成影响

线程异常

当其中一个线程出现异常时,OS发送的信号会终止整个进程!这也是多线程健壮性低的原因之一

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <string>
#include <assert.h>
#include <unistd.h>
using namespace std;

void* thread(void* args)//线程要执行的任务
{
    while(true)
    {
       printf("我是一个线程\n");
       sleep(1);
       int a = 1 / 0;//除零异常
    }
}

int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid,nullptr,thread,(void*)"无敌进程");
    assert(ret == 0);//线程正常被创建时返回0
    while(true)
    {
        printf("我是一个进程\n");
        sleep(1);
    }
    return 0;
}

线程控制

在pthread.h库中,所有跟线程有关的接口,都是以pthread_开头(例如pthread_create)

当pthread的接口们出错时不会设置errno全局变量 (若设置,所有线程就都能看到),而是将错误代码通过返回值返回(虽然线程内部也有自己的errno,但不建议使用)

上面只实现了2个线程的进程,下面实现一下多线程的进程

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
using namespace std;

class ThreadData//用于存储线程信息的类
{
public:
    pthread_t pid;
    char buffer[64];
};

void* start(void* args)//线程执行的函数
{
    int cnt = 10;
    ThreadData* th = static_cast<ThreadData*>(args);//C++风格的类型转换,更安全
    while(cnt)
    {
        printf("我是一个线程:%s|||cnt:%d\n",th->buffer,cnt--);
        sleep(1);
    }
    return nullptr;
}

int main()
{
    vector<ThreadData*> tdv;
    for(int i = 0;i < 10; i++)//创建10个新线程
    {
        ThreadData* th = new ThreadData;//如果不用指针,可能会出现在线程全部创建完再执行线程的情况,就会让所有线程传进去的arg都一样
        snprintf(th->buffer,sizeof(th->buffer),"%s:%d","thread",i+1);
        int n = pthread_create(&th->pid,nullptr,start,(void*)th);
        assert(n == 0);
        tdv.push_back(th);//将线程以数组方式管理
    }
    while(true)
    {
        printf("我是一个进程\n");
        sleep(1);
    }

    for(const auto& td : tdv)//释放用于存储线程信息的指针
    {
        delete td;
    }
        
    return 0;
}

在上面程序中,我们的10个线程都执行的同一个函数,也就是说一个函数在被10个线程同时运行,因此该函数的状态就是重入状态 ,该函数就可以暂时理解为可重入函数 (严格来说printf或cout是不可重入函数,但这里暂时先不谈),每个线程都要定义一次cnt,每个线程都要对cnt--,就说明它们肯定不是一个cnt,每个cnt的虚拟地址都不一样

因此,每个线程都有自己独立的栈结构(函数的局部变量也类似道理)

线程终止

线程终止有三种方法,第一种就是像上面那样return返回时线程就会结束

pthread_exit()

还有一种方式是调用pthread库中的**pthread_exit()**函数

参数retval就是要返回的值,将上面线程中的return换成pthread_exit,结果是一样的

cpp 复制代码
void* start(void* args)//线程执行的函数
{
    int cnt = 10;
    ThreadData* th = static_cast<ThreadData*>(args);//C++风格的类型转换,更安全
    while(cnt)
    {
        printf("我是一个线程:%s|||cnt:%d,&cnt:%p\n",th->buffer,cnt--,&cnt);
        sleep(1);
    }
    pthread_exit(nullptr);
}

为什么不能直接用exit()返回?因为++exit()会直接终止进程,将所有线程一起退出!++

pthread_cancel()

pthread_cancel可以取消一个正在运行的线程,被取消的线程的返回值为-1

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void* start(void* args)
{
    while(true)//死循环
    {
        cout << "我是一个线程\n";
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start,(void*)"无敌线程");//创建线程
    sleep(3);
    pthread_cancel(tid);//取消线程
    return 0;
}

当3秒过后,线程被取消

线程等待

既然线程结束有返回值,如果想要查看它,就需要线程等待

如果线程不等待,它的PCB也不会被释放,就会造成类似僵尸进程的问题------内存泄漏

当线程被等待,就可以获取线程的退出信息 ,并回收线程PCB等内核资源

线程等待的接口是pthread_join()

参数thread为被等待线程的ID,参数retval 为输出性参数,接收线程的void*类型返回值

它是阻塞式调用的,若被等待的线程还没有退出,就会阻塞

cpp 复制代码
int main()
{
    vector<ThreadData*> tdv;
    for(int i = 0;i < 10; i++)//创建10个新线程
    {
        ThreadData* th = new ThreadData;//如果不用指针,可能会出现在线程全部创建完再执行线程的情况,就会让所有线程传进去的arg都一样
        snprintf(th->buffer,sizeof(th->buffer),"%s:%d","thread",i+1);
        int n = pthread_create(&th->pid,nullptr,start,(void*)th);
        assert(n == 0);
        tdv.push_back(th);//将线程以数组方式管理
    }

    for(const auto& td : tdv)//等待线程
    {
        pthread_join(td->pid,nullptr);
        cout << td->pid << "等待成功\n";
    }

    for(const auto& td : tdv)//释放用于存储线程信息的指针
        delete td;

    
    return 0;
}

由于在线程中的返回值为空,因此这里接收时也是空

如果线程有返回值,在线程结束后,会将返回值++暂存到pthread库中的一个void*变量中++,到线程等待时,再从该变量中拿出来

cpp 复制代码
void* start(void* args)//线程执行的函数
{
    int cnt = 10;
    ThreadData* th = static_cast<ThreadData*>(args);//C++风格的类型转换,更安全
    while(cnt)
    {
        printf("我是一个线程:%s|||cnt:%d,&cnt:%p\n",th->buffer,cnt--,&cnt);
        sleep(1);
    }
    pthread_exit((void*)"114514");
}

int main()
{
    //......
    for(const auto& td : tdv)//等待线程
    {
        pthread_join(td->pid,&ret);
        cout << td->pid << "等待成功:" << (char*)ret << endl;
    }
    //......
}

既然整数都可以当做地址返回,当有真正的地址需要返回(例如对象,堆空间)时,也可以用pthread_join获取:

cpp 复制代码
class ThreadReturn//线程返回值
{
public:
    int exit_code;//退出码
    int exit_result;//退出结果
};
//......
void* start(void* args)//线程执行的函数
{
    int cnt = 10;
    ThreadData* td = static_cast<ThreadData*>(args);//C++风格的类型转换,更安全
    while(cnt)
    {
        printf("我是一个线程:%s|||cnt:%d,&cnt:%p\n",td->buffer,cnt--,&cnt);
        sleep(1);
    }
    ThreadReturn* tr = new ThreadReturn();
    tr->exit_code = 0;
    tr->exit_result = 114514;
    pthread_exit((void*)tr);
}

int main()
{
    //......
    for(const auto& td : tdv)//等待线程
    {
        pthread_join(td->pid,(void**)&ret);
        cout << td->pid << "等待成功:code->" << ret->exit_code << " result->" << ret->exit_result << endl;
    }
    //......
}

为什么一定要返回的是指针呢?如果返回的是一个在线程内部的局部变量,当线程结束后,该变量也会被销毁 ,但动态开辟的内存空间不一样,它只有等到进程结束或delete时才会释放

在进程退出时,等待回收子进程会获取它的退出码和退出信号 ,但在线程退出时没有退出信号这一说法 ,++这是因为当接受到终止信号时,会终止整个进程!++

如果是用**pthread_cancel()**取消的线程,该线程的返回值是-1

线程分离

线程默认状态是joinable,线程终止后资源不会被自动回收,需由其他线程调用pthread_join回收。

若该线程没有什么值得被等待的信息,就将线程的状态设置为detached ,线程终止后资源会自动回收,不能通过pthread_join获取返回值 。这就是线程分离,需要用到pthread_detach(),参数中传的就是要分离的线程ID

可以在该线程外对它设置分离 ,也可以在线程内对自己设置分离

若是在要分离的线程外设置分离,就直接用pthread_create时传入的输出型参数tid即可

cpp 复制代码
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void* start(void* args)//线程要执行的函数
{
    int cnt = 10;
    while(cnt--)
    {
        cout << "我是一个新线程,cnt:" << cnt << endl;
        sleep(1);
    }
    return (void*)114514;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start,(void*)"无敌线程");//创建线程
    sleep(4);
    pthread_detach(tid);//分离线程
    void* ret;
    int n = pthread_join(tid,&ret);//分离线程后就不允许被等待了
    if(n != 0)
    {
        cout << "线程等待失败:" << strerror(n) << endl;
    }
    return 0;
}

或者也可以在线程内部为自己设置分离

cpp 复制代码
void* start(void* args)//线程要执行的函数
{
    int cnt = 10;
    while(cnt--)
    {
        cout << "我是一个新线程,cnt:" << cnt << endl;
        sleep(1);
        if(cnt == 6)
        {
            pthread_detach(pthread_self());//pthread_self()可以获取自己的tid
        }
    }
    return (void*)114514;
}
  • 注意: 如果在新线程内设置分离自己,在主线程内等待时,可能并不会报错。这是因为主线程和新线程不一定谁会先运行,如果主线程先运行,到pthread_join那一行时,并不知道新线程要设置分离状态,因此还是会陷入阻塞等待

线程ID

在创建线程时,我们用pthread库创建了多个线程,别的进程也可能会用pthread库创建多个线程,因此线程也需要被管理 :++先描述,再组织++。描述的就是线程的属性,也就是pthread_create()中的pthread_attr_t类型的attr参数

它是一个联合体,里面其实有各种关于线程的属性,只不过这里不可见,可以由对应API来实现对参数的设置

每创建一个线程(轻量级进程(LWP)),相对应的就需要在pthread库中创建一个用户级线程 (线程控制块(TCB)),它们是一对一的关系

这些TCB存储在进程地址空间的****共享区,里面有指向LWP的ID,线程局部存储信息,线程栈信息等等 ,而我们在pthread_create()传入的输出型参数tid,就是该线程所对应TCB的起始地址

在进行线程控制的相关操作时,就是通过tid找到TCB的起始地址,进而找到该线程的属性

线程栈:上面说过,每个线程都有自己的私有栈,这个栈结构就是在进程地址空间的共享区的TCB中,该栈为固定大小,不能扩容

线程局部存储:对于全局变量,正常情况来说是所有线程共享一份的

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

int global = 100;//全局变量

void* start(void* args)//线程要执行的函数
{
    while(true)
    {
        printf("我是一个新线程,global:%d,&global:%p\n",global++,&global);//每次对global+1
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start,(void*)"无敌线程");//创建线程
    pthread_detach(tid);//分离线程,让该线程不需要被等待
    while(true)
    {
        printf("我是主线程,global:%d,&global:%p\n",global,&global);
        sleep(1);
    }
    return 0;
}

上面程序中,当新线程对全局变量global进行+1后,主线程再访问global也会是+1后的结果,并且新线程和主线程的global地址是一样的

但如果不想将该全局变量让所有线程共享一份,就可以将该全局变量设为线程局部存储

若想对一个内置类型的变量设为线程局部存储,可以在定义该变量的前面加上**__thread**

cpp 复制代码
__thread int global = 100;//全局变量

再运行时,主线程和新线程的global的地址不一样,并且值也不一样

上面提到过,线程局部存储也在共享区,由于地址是从低到高,作为全局变量,它的地址是在已初始化数据区,因此作为全局变量时的global地址很短;作为线程局部存储,它的地址在共享区,因此作为线程局部存储时的global地址很长

C++线程库

C++有内置的线程库,为thread库

在Windows环境下,可以直接用该库创建线程,但在Linux下,Linux的线程是通过pthread库实现的 ,C++的thread库在Linux中也是需要用到pthread的函数,因此在编译时依旧必须指定pthread库 。pthread是Linux的原生线程库,C++的thread线程库本质上是对pthread库的封装

cpp 复制代码
#include <iostream>
#include <thread>//C++的线程库
#include <unistd.h>
using namespace std;

void start()//线程要执行的函数
{
    while(true)//死循环
    {
        cout << "我是一个线程\n";
        sleep(1);
    }
}

int main()
{
    thread t1(start);//创建线程
    sleep(3);
    t1.join();//等待线程
    return 0;
}

封装线程库

若是从C++线程库和pthread线程库中选一个,大部分人都会选C++线程库,这是因为它的操作简单,不像pthread库中的函数一样复杂,因此下面也来简单实现一下pthread线程库的封装,让它实现类似C++线程库的效果

Thread.hpp:

cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>
#include <string>
#include <functional>
#include <cstring>

class Thread//线程类
{
public:
    using func_t = std::function<void *(void *)>; // 将void* (void*)类型重命名为func_t

    Thread(func_t start, std::string name, void *args = nullptr) // 构造函数
        : _start(start), _name(name), _args(args)
    {
        _tid = 0;
        start_routine();//实例化时创建线程
    }

    void* join() // 等待线程
    {
        void *ret;
        int n = pthread_join(_tid, &ret);
        if(n != 0)//等待失败
        {
            std::cout << "等待失败,错误码:" << n << ",错误信息:" << strerror(n);
            return nullptr;
        }
        else//等待成功
            _tid = 0;//让tid为零,这样当析构时就不会再次等待
        return ret;
    }

    ~Thread()
    {
        //没有什么需要释放的资源
    }

private:
    class Context // 线程的上下文
    {
    public:
        Thread *_this; // 实例化的线程
        void *_args;   // 线程的参数

        Context(Thread *thr, void *args) // 构造函数
            : _this(thr), _args(args)
        {
        }
    };

    void start_routine() // 创建线程,因为在构造函数中就会调用,所以不需要被用户看到
    {
        Context *ctx = new Context(this, _args);
        int n = pthread_create(&_tid, nullptr, _start_routine, (void *)ctx);
        if (n != 0) // 创建线程失败
        {
            std::cout << "创建线程失败!错误码:" << n << "错误信息:" << strerror(n) << std::endl;
            delete ctx;
            return;
        }
    }

    static void* _start_routine(void *args)
    {
        Context *ctx = static_cast<Context *>(args); // 安全的类型转换
        void *re = ctx->_this->_start(ctx->_args);   // re用于保存线程的返回值
        delete ctx;
        return re;
    }
    std::string _name; // 线程名称
    pthread_t _tid;    // 线程tid
    func_t _start;     // 线程要执行的函数
    void *_args;       // 传给_start的参数
};

需要注意几点:

  1. _start_routine成员方法设为static ,是因为它要被用于pthread_create函数中让线程执行的函数,该函数只允许是void*返回值,void*作参数,而如果不加static,_start_routine函数的参数还会有一个this指针!

2.创建Context类,是为了将线程属性传入线程要执行的函数(线程属性中的start是一个包装器,并不是原生的函数指针,传不进pthread_create的参数中,因此需要靠Context类,将全部的线程属性作为参数传进线程要执行的函数中)

相关推荐
oLLI PILO2 小时前
在linux(Centos)中Mysql的端口修改保姆级教程
linux·mysql·centos
小张成长计划..2 小时前
【C++】23:封装set和map
c++
满天星83035772 小时前
【Linux/多路复用】select
linux·运维·服务器·c语言·c++
我叫唧唧波2 小时前
【自动化部署】CI/CD 实战(三):让 Argo CD 接管 CD,Jenkins 镜像自动同步到集群
运维·前端·ci/cd·docker·自动化·jenkins·argocd
t***5442 小时前
如何验证Clang是否在Dev-C++中正常工作
开发语言·c++
dualven_in_csdn2 小时前
【docker】docker下如何使用宿主主机的GPU
运维·docker·容器
cyber_两只龙宝2 小时前
【Oracle】Oracle之SQL的集合运算符
linux·运维·数据库·sql·云原生·oracle
Hello.Reader2 小时前
Ubuntu 安装 Miniconda 完整从零开始把 Conda 环境搭起来
linux·ubuntu·conda
charlie1145141912 小时前
嵌入式C++开发第17篇:C++23特性收尾 —— 属性、链接与零开销抽象的最终证明
开发语言·c++·stm32·学习·c++23