Linux-线程控制

**摘要:**本文围绕 Linux 进程与线程的关系展开,先对比进程间资源独占与线程间资源共享的差异,说明进程是资源分配的基本单位、线程是调度的基本单位,并梳理多线程共享的地址空间、文件描述符表、信号处理方式等资源。随后系统讲解 pthread 线程控制接口,包括 pthread_create 创建线程、pthread_join 等待线程、pthread_exit 与 pthread_cancel 终止线程、pthread_self 获取线程编号,以及线程传参与返回值的实现原理,并深入分析线程 ID 的本质、线程崩溃导致整个进程退出的原因。最后介绍 pthread_detach 分离线程的使用与最佳实践、C++ std::thread 对 pthread 库的封装,以及通过 __thread 实现线程局部存储的方法。

目录

1.Linux进程和线程

1.1资源共享与独占

1.2进程和线程

1.3进程的多个线程共享

2.线程控制

2.1相关接口

2.1.1创建线程

2.1.1.1使用接口

2.1.1.2观察接口参数

2.1.1.3验证线程资源共享

2.1.1.4线程结束结果

2.1.2等待线程

2.1.2.1使用接口

2.1.2.2等待多线程

2.1.2.3多线程命名

2.1.2.4线程崩溃问题

2.1.3终止(退出)线程

2.1.3.1第一个接口

2.1.3.2第二个接口

2.1.4获取线程编号

2.1.4.1接口的使用

2.1.4.2多接口使用

2.1.5线程的传参和返回值

2.1.5.1编码层面

2.1.5.2pthread_join第二个参数

2.1.5.3应用层面

2.1.6分离线程

2.1.6.1基本概念和使用

2.1.6.2返回值问题

2.2C++的线程

2.3线程局部存储


1.Linux进程和线程

1.1资源共享与独占

①进程间具有独立性;

②线程共享地址空间,也就共享进程资源。

进程间具有独立性,每个进程都有独立的虚拟地址空间,独立的PCB,独立的页表,独立的代码和数据,因此每个进程的很多东西都是互相解耦的,这是进程的优点。

但同时,进程间独立也带来了缺点:资源共享成本比较高,因此需要依靠进程间通信的技术。

线程在设计时,天然根据操作系统的理念设计出来的,Linux的线程必须要资源共享,但是如果没有处理好,就可能导致进程崩溃的问题!

因此,进程间资源基本独占,偶尔需要共享;线程间资源基本共享,偶尔需要独占,它们两个侧重点不同。

1.2进程和线程

①进程是资源分配的基本单位,线程是调度的基本单位;

②线程共享进程数据,但也拥有一部分"私有"数据:线程ID,一组寄存器,线程上下文数据,栈,errno,信号屏蔽字,调度优先级。

实际上的线程并没有私有数据,只不过这部分资源分配给了这个线程而已,如果其他线程想要拿这个数据也是能拿到的。

1.3进程的多个线程共享

因为每个线程共用同一个地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,各线程中都可以调用;如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

①文件描述符表;②各种信号的处理方式(SIG_IGN,SIG_DFL或者自定义的信号处理函数);③当前工作目录;④用户id和组id。

进程和线程相关关系如下图:

2.线程控制

2.1相关接口

在学习相关接口前,提醒一下:要使用这些接口,必须包含头文件<pthread.h>,如果编译不了,就需要在最后链接一下动态库:-lpthread。

2.1.1创建线程

2.1.1.1使用接口

创建线程的接口之前学过:

cpp 复制代码
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

函数的具体说明可以在命令行输入以下命令:

bash 复制代码
man pthread_create

它的第一个参数是输出型参数,它会输出所创建的线程的ID,这个是不是和我们之前用:

bash 复制代码
ps -aL | head -1 && ps -aL | grep thread

看到的LWP呢?

第一个参数返回的并不是LWP,因为LWP并不需要暴露给用户,我们现在只需要认为它是一个ID值,具有唯一性即可,具体的解释后面再说!

第二个参数attr:你要创建的线程的属性,可以设置,但现在我们只需要设置为NULL即可,如果传入的是NULL,它会自己设置默认的,大部分情况下是不用我们自己设置的!

第三个参数是一个回调函数,这个参数是一个返回值 为void*参数是void*的函数指针类型,我们将来想要线程做什么事情就可以传入一个函数指针让线程去执行!第四个参数是第三个参数的函数的参数!

这个接口的使用如下:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>

//新线程执行函数
void* thread_routine(void* args)
{
    //拿到参数
    std::string name = (const char*)args;
    //也不知道做什么就死循环
    while(true)
    {
        std::cout << "我是新线程...,名字:" << name << std::endl;
        sleep(1);
    }
}

int main()
{
    //线程id
    pthread_t tid;
    //创建线程
    pthread_create(&tid,nullptr,thread_routine,(void*)"thread-1");
    //主线程继续往后走
    //现在做不了什么事情就死循环
    while(true)
    {
        std::cout << "我是主线程..." << std::endl;
        sleep(1);
    }
    return 0;
}

编译后运行有:

至于返回值,大部分以pthread开头的接口成功都是返回0,失败时会以错误码进行返回,不同的错误码代表不同的失败原因,如果各位想要接受返回值可以修改代码!

2.1.1.2观察接口参数

所以那个pthread_t到底是什么,我们转到定义:

cpp 复制代码
/* Thread identifiers.  The structure of the attribute type is not
   exposed on purpose.  */
typedef unsigned long int pthread_t;

它是一个无符号长整数,如果想要看到,加个打印即可:

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<string>

//新线程执行函数
void* thread_routine(void* args)
{}

int main()
{
    //线程id
    pthread_t tid;
    //创建线程
    pthread_create(&tid,nullptr,thread_routine,(void*)"thread-1");
    //看一下这个数是什么
    std::cout << tid << std::endl;
    return 0;
}

可能会报警告,但是我们不用管,运行结果为:

因此,这个并不是我们看到的LWP,正常的LWP应该是只有4-5位数字,这里有15位!

这个值这样看是不好看的,因此我们要打印需要以16进制打印:

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<string>

//新线程执行函数
void* thread_routine(void* args)
{}

int main()
{
    //线程id
    pthread_t tid;
    //创建线程
    pthread_create(&tid,nullptr,thread_routine,(void*)"thread-1");
    //看一下这个数是什么
    printf("%lx\n",tid);
    return 0;
}

我们现在可以把这个参数理解成一个地址,但是这个地址现在还说不了!

2.1.1.3验证线程资源共享
cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>

//定义全局变量
volatile int gval = 100;

//新线程执行函数
void* thread_routine(void* args)
{
    //拿到参数
    std::string name = (const char*)args;
    //也不知道做什么就死循环
    while(true)
    {
        std::cout << "我是新线程...,gavl:" << gval << "&gval:" << &gval << std::endl;
        //只让新线程做++
        gval++;
        sleep(1);
    }
}

int main()
{
    //线程id
    pthread_t tid;
    //创建线程
    pthread_create(&tid,nullptr,thread_routine,(void*)"thread-1");
    //主线程继续往后走
    while(true)
    {
        std::cout << "我是主线程...,gavl:" << gval << "&gval:" << &gval << std::endl;
        sleep(1);
    }
    return 0;
}

运行有:

我们看到:两个线程的gval都会变化,结论:两个线程看到全局变量的地址是一样的,即每个线程共享全局变量!

那么它们两个线程会不会共享一个函数呢?

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>

//定义全局的函数
std::string fun()
{
    return "我是另一个函数";
}

//新线程执行函数
void* thread_routine(void* args)
{
    //拿到参数
    std::string name = (const char*)args;
    //也不知道做什么就死循环
    while(true)
    {
        std::cout << "我是新线程...," << fun() << std::endl;
        //只让新线程做++
        //gval++;
        sleep(1);
    }
}

int main()
{
    //线程id
    pthread_t tid;
    //创建线程
    pthread_create(&tid,nullptr,thread_routine,(void*)"thread-1");
    //主线程继续往后走
    while(true)
    {
        std::cout << "我是主线程...," << fun() << std::endl;
        sleep(1);
    }
    return 0;
}

运行有:

因此,多线程:其他函数共享!也就是说:栈区是共享的!

那么堆区的数据共享吗?

如果这样写代码:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>

//定义指针
int* data = nullptr;

//新线程执行函数
void* thread_routine(void* args)
{
    //拿到参数
    std::string name = (const char*)args;
    //只在新线程申请空间
    data = new int(10);
    //也不知道做什么就死循环
    while(true)
    {
        sleep(1);
    }
}

int main()
{
    //线程id
    pthread_t tid;
    //创建线程
    pthread_create(&tid,nullptr,thread_routine,(void*)"thread-1");
    //主线程继续往后走
    while(true)
    {
        std::cout << "我是主线程" << *data << std::endl;
        sleep(1);
    }
    return 0;
}

那么你大概率会出现这样的情况:

我们知道Segmentation fault:段错误,意思是你想要访问一块没有权限访问的地址,一般来说,当空指针解引用就会出现这种情况!

这也就侧面证明了:如果一个线程出问题导致操作系统发信号,所有的线程都会被信号所终止!

那么导致这个问题的原因到底是本来堆区不共享资源还是什么其他原因?

用以下代码试一下:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>

//定义指针
int* data = nullptr;

//新线程执行函数
void* thread_routine(void* args)
{
    //拿到参数
    std::string name = (const char*)args;
    //只在新线程申请空间
    data = new int(10);
    //死循环
    while(true)
    {
        sleep(1);
    }
}

int main()
{
    //线程id
    pthread_t tid;
    //创建线程
    pthread_create(&tid,nullptr,thread_routine,(void*)"thread-1");
    //主线程继续往后走
    //需要先休眠再打印
    sleep(2);
    //死循环
    while(true)
    {
        std::cout << "我是主线程" << *data << std::endl;
        sleep(1);
    }
    return 0;
}

我们发现能够成功运行:

这是因为主线程和新线程它们两个执行顺序不是固定的,因此我们需要先让主线程休眠几秒钟,让新线程执行完申请空间的操作才行!

这也就侧面证明了:原则上,线程间堆空间也是共享的!

实际上,还包括虚拟地址空间上的其他区域,命令行参数和环境变量和操作系统本身都是被所有线程共享的!

为什么说原则上,堆空间共享?

如果我们在新线程的函数内部定义指针,那么就会导致主线程看不到,编译时就会报错:

也就是说:在线程所执行的函数内部定义的变量,此时的堆空间是不共享的,因为函数是有独立的栈,这个变量是在栈上开辟的,因此只有定义这个变量的线程才能访问这个变量。

因为堆空间是线程自己申请的,往往只需要让对应的线程知道这部分堆空间的起始虚拟地址,就可以叫做这个堆是该线程拥有的!

2.1.1.4线程结束结果

那么如果新创建的线程不是死循环即如果线程执行完了自己的入口函数,表明该线程结束;如果main函数结束了表示主线程结束,同时也表示当前进程结束,因此所有的线程都会跟着结束:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>

//新线程执行函数
void* thread_routine(void* args)
{
    //拿到参数
    std::string name = (const char*)args;
    //新线程不退
    while(true)
    {
        std::cout << "我是新线程" << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    //线程id
    pthread_t tid;
    //创建线程
    pthread_create(&tid,nullptr,thread_routine,(void*)"thread-1");
    int n = 3;
    //主线程过3秒退
    while(n--)
    {
        sleep(1);
    }
    return 0;
}

那么就会有:

虽然新线程是死循环,但是因为主线程退出导致进程结束,所以新线程也结束了!且主线程结束会释放所有资源,因为所有资源包含新线程的task_struct/地址空间/页表......所以其他线程也没法运行了!即:进程结束,所有线程全部退出,哪怕代码也没执行完!因此,我们需要保证主线程最后退出避免以后发生这种情况!

如果新线程不是死循环,那么主线程会不会退?

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>

//新线程执行函数
void* thread_routine(void* args)
{
    //拿到参数
    std::string name = (const char*)args;
    //新线程过3秒退
    int n = 3;
    while(n--)
    {
        std::cout << "我是新线程" << std::endl;
        sleep(1);
    }
    std::cout << "新线程退了" << std::endl;
    return nullptr;
}

int main()
{
    //线程id
    pthread_t tid;
    //创建线程
    pthread_create(&tid,nullptr,thread_routine,(void*)"thread-1");
    //主线程不退
    while(true)
    {
        std::cout << "我是主线程" << std::endl;
        sleep(1);
    }
    return 0;
}

因此,新线程的退出了,主线程仍然能继续运行!

不过各位可以试一下用:

bash 复制代码
while :; do ps -aL; sleep 1 ;done

开两个窗口观察一下变化,也能验证这个结论!

因此,其他线程退了没有什么问题,但是就是不能让主线程退了,会导致很多问题,这就要求我们需要进行线程等待类似进程等待一样!最好是让主线程等待新线程的退出,如果不等待新线程退出,就会造成类似僵尸进程的问题!

2.1.2等待线程

2.1.2.1使用接口

因为需要等待线程的退出,所以需要使用这个接口:

cpp 复制代码
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

这个接口是用来等待线程的,它是阻塞式等待线程退出的,这样就能保证主线程最后一个退出!如果等待线程成功,会自动解决新线程的内存泄漏 ----- 僵尸问题!

第一个参数:输入型参数,传入一个线程的ID,会让执行该函数的线程等待对应的线程,就是pthread_create的第一个参数,第二个参数是输出型参数,我们创建进程和线程的目的是为了让它们给我们做事情,因此就需要知道它们把任务完成得怎么样,这个参数就是获取新线程的退出信息,和进程不一样的地方是,它只是退出信息,没有退出码!因为新线程的返回值为void*,为了得到返回值,需要使用void**来获取!如果不关心退出信息,我们设置为NULL/nullptr即可!

对于返回值,如果成功,就返回0,如果失败,就返回出错时的错误码。

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>

//新线程执行函数
void* thread_routine(void* args)
{
    //拿到参数
    std::string name = (const char*)args;
    //新线程过3秒退
    int n = 3;
    while(n--)
    {
        std::cout << "我是新线程" << std::endl;
        sleep(1);
    }
    std::cout << "新线程退了" << std::endl;
    return nullptr;
}

int main()
{
    //线程id
    pthread_t tid;
    //创建线程
    pthread_create(&tid,nullptr,thread_routine,(void*)"thread-1");
    //等待新线程退出
    //不关心退出信息
    pthread_join(tid,nullptr);
    std::cout << "主线程退出" << std::endl;
    return 0;
}

运行有:

2.1.2.2等待多线程

如果我要创建3个线程呢,该怎么操作:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <vector>

// 新线程执行函数
void *thread_routine(void *args)
{
    // 拿到参数
    std::string name = (const char *)args;
    // 新线程过2秒退
    int n = 2;
    while (n--)
    {
        std::cout << "我是新线程" << std::endl;
        sleep(1);
    }
    std::cout << "新线程退了" << std::endl;
    return nullptr;
}

// 创建线程数
int gnum = 3;

int main()
{
    // 所有线程id
    std::vector<pthread_t> tids;
    // 创建一批线程
    for (int i = 0; i < gnum; i++)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
        tids.push_back(tid);
    }
    // 等待一批新线程退出
    for (auto &tid : tids)
    {
        // 不关心退出信息
        pthread_join(tid, nullptr);
        std::cout << "退出的线程是:" << tid << std::endl;
    }
    std::cout << "主线程退出" << std::endl;
    return 0;
}
2.1.2.3多线程命名

如果我想要每个线程有自己的名字,可以这样写:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include<cstdio>
#include <vector>

// 新线程执行函数
void *thread_routine(void *args)
{
    // 拿到参数
    std::string name = (const char *)args;
    // 新线程过2秒退
    int n = 2;
    while (n--)
    {
        std::cout << "我是新线程" << name << std::endl;
        sleep(1);
    }
    std::cout << "新线程" << name << "退出" << std::endl;
    return nullptr;
}

// 创建线程数
int gnum = 3;

int main()
{
    // 所有线程id
    std::vector<pthread_t> tids;
    // 创建一批线程
    for (int i = 0; i < gnum; i++)
    {
        pthread_t tid;
        //构建新线程的名称
        //但是这样传递的内容是共享的
        char idbuffer[64];
        //用snprintf构建
        snprintf(idbuffer,sizeof(idbuffer),"thread-%d",i+1);
        pthread_create(&tid, nullptr, thread_routine, idbuffer);
        tids.push_back(tid);
    }
    // 等待一批新线程退出
    for (auto &tid : tids)
    {
        pthread_join(tid, nullptr);
        std::cout << "退出的线程是:" << tid << std::endl;
    }
    std::cout << "主线程退出" << std::endl;
    return 0;
}

但是如果我们运行一下就会发现问题:

多运行几遍:

也就是说,每个线程的名字是一样的,这是什么原因?

我们知道,如果在main的第一个循环里定义的char idbuffer是一个临时变量,当构建第一个线程的id时为thread-1,然后新线程执行thread_routine函数,并把这个函数名传入到线程1里,然后主线程继续执行下一个循环,因此就把那个idbuffer释放掉了,线程1还没拿到自己的名字这个名字就没了,因此,当新线程被创建时,没等到新线程把自己的名字强转为字符串主线程就把对应的空间释放了!CPU运行速度特别快,还没等到第一个新线程开始强转,最后一个线程已经被创建好了,因此,所有的线程拿到的线程名都是最后一个线程的线程名,因为资源共享,所以所有的线程都能拿到最后的idbuffer里的内容!

因此,我们需要给每一个线程单独创建一个缓冲区,因为堆空间不一定共享,所以说这样就能保证每一个线程拿到的都是独一无二的名字!

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include<cstdio>
#include <vector>

// 新线程执行函数
void *thread_routine(void *args)
{
    // 拿到参数
    std::string name = (const char *)args;
    // 新线程过2秒退
    int n = 2;
    while (n--)
    {
        std::cout << "我是新线程" << name << std::endl;
        sleep(1);
    }
    std::cout << "新线程" << name << "退出" << std::endl;
    return nullptr;
}

// 创建线程数
int gnum = 3;

int main()
{
    // 所有线程id
    std::vector<pthread_t> tids;
    // 创建一批线程
    for (int i = 0; i < gnum; i++)
    {
        pthread_t tid;
        //构建新线程的名称
        char* p = new char[64];
        //用snprintf构建
        snprintf(p,64,"thread-%d",i+1);
        pthread_create(&tid, nullptr, thread_routine, p);
        tids.push_back(tid);
    }
    // 等待一批新线程退出
    for (auto &tid : tids)
    {
        pthread_join(tid, nullptr);
        std::cout << "退出的线程是:" << tid << std::endl;
    }
    std::cout << "主线程退出" << std::endl;
    return 0;
}

虽然说这样能保证每个线程的名字不重复,但是线程调度是不确定的,因此就显示不整齐!

2.1.2.4线程崩溃问题
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include<cstdio>
#include <vector>

// 新线程执行函数
void *thread_routine(void *args)
{
    // 拿到参数
    std::string name = (const char *)args;
    int cnt = 5;
    // 新线程不退出
    while (true)
    {
        std::cout << "我是新线程" << name << std::endl;
        cnt--;
        if(cnt == 3)
        {
            std::cout << name << "退出" << std::endl;
            int* p = nullptr;
            //故意对空指针解引用
            *p = 0;
        }
        sleep(1);
    }
    return nullptr;
}

// 创建线程数
int gnum = 3;

int main()
{
    // 所有线程id
    std::vector<pthread_t> tids;
    // 创建一批线程
    for (int i = 0; i < gnum; i++)
    {
        pthread_t tid;
        //构建新线程的名称
        char* p = new char[64];
        snprintf(p,64,"thread-%d",i+1);
        pthread_create(&tid, nullptr, thread_routine, p);
        tids.push_back(tid);
    }
    // 等待一批新线程退出
    for (auto &tid : tids)
    {
        pthread_join(tid, nullptr);
        std::cout << "退出的线程是:" << tid << std::endl;
    }
    std::cout << "主线程退出" << std::endl;
    return 0;
}

因此,只要有一个线程出问题导致崩溃,其他线程包括主线程在内也全部都崩溃!

所以我们之前说线程的缺点的时候,代码的健壮性降低!之前我也演示过!

这样写的代码是多线程的,因此,为了演示方便,后续还是以单线程为例:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include<cstdio>
#include <vector>

// 新线程执行函数
void *thread_routine(void *args)
{
    // 拿到参数
    std::string name = (const char *)args;
    // 新线程不退出
    while (true)
    {
        std::cout << "我是新线程" << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    //新线程id
    pthread_t tid;
    // 创建线程
    pthread_create(&tid, nullptr, thread_routine, (void*)"thread-1");
    // 等待一批线程退出
    pthread_join(tid, nullptr);
    std::cout << "主线程退出" << std::endl;
    return 0;
}

如果想要退出新线程,不能用exit函数,它是用来退出进程的,如果想要用其他方法退出线程,还需要用其他的接口!

2.1.3终止(退出)线程

2.1.3.1第一个接口
cpp 复制代码
#include <pthread.h>
void pthread_exit(void *retval);

这个接口的作用是:哪个线程调用该接口,哪个线程就结束,参数是void*类型,输出型参数,代表该线程的退出信息,如果不关心,设置为NULL即可:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>

//线程退出
// 新线程执行函数
void *thread_routine(void *args)
{
    // 拿到参数
    std::string name = (const char *)args;
    // 新线程执行1秒就退出
    while (true)
    {
        std::cout << "我是新线程" << std::endl;
        sleep(1);
        break;
    }
    pthread_exit(nullptr);
}

int main()
{
    //新线程id
    pthread_t tid;
    // 创建线程
    pthread_create(&tid, nullptr, thread_routine, (void*)"thread-1");
    // 等待一批线程退出
    pthread_join(tid, nullptr);
    std::cout << "主线程退出" << std::endl;
    return 0;
}

开始时:

一秒后:

2.1.3.2第二个接口
cpp 复制代码
#include <pthread.h>
int pthread_cancel(pthread_t thread);

这个接口可以让一个线程去取消(终止)指定线程,参数是取消线程的线程编号。若取消成功,则会返回0,如果失败,就返回错误码!

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>

//取消线程
// 新线程执行函数
void *thread_routine(void *args)
{
    // 拿到参数
    std::string name = (const char *)args;
    // 新线程执行1秒就退出
    while (true)
    {
        std::cout << "我是新线程,ID:" << pthread_self() << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    //新线程id
    pthread_t tid;
    // 创建线程
    pthread_create(&tid, nullptr, thread_routine, (void*)"thread-1");
    //等待两秒后取消线程
    sleep(2);
    pthread_cancel(tid);
    // 等待线程退出
    pthread_join(tid, nullptr);
    std::cout << "主线程退出" << std::endl;
    return 0;
}

如果想要观察更多的细节,可以开两台机器,然后另一台机器执行:

bash 复制代码
while :; do ps -aL; sleep 1 ;done

但是现在有一个问题,每个线程只能看到别人的线程ID,但不能看到自己线程的ID,因此就需要一个接口!

2.1.4获取线程编号

2.1.4.1接口的使用
cpp 复制代码
#include <pthread.h>
pthread_t pthread_self(void);

这个接口用于获取调用该接口的线程的ID:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>

//获取线程ID
// 新线程执行函数
void *thread_routine(void *args)
{
    // 拿到参数
    std::string name = (const char *)args;
    // 新线程执行1秒就退出
    while (true)
    {
        std::cout << "我是新线程,ID:" << pthread_self() << std::endl;
        sleep(1);
        break;
    }
    return nullptr;
}

int main()
{
    //新线程id
    pthread_t tid;
    // 创建线程
    pthread_create(&tid, nullptr, thread_routine, (void*)"thread-1");
    //获取新线程ID
    std::cout << "主线程得到的新线程ID:" << tid << std::endl;
    //获取主线程ID
    std::cout << "我是主线程,ID:" << pthread_self() << std::endl;
    // 等待一批线程退出
    pthread_join(tid, nullptr);
    std::cout << "主线程退出" << std::endl;
    return 0;
}

那么结合之前的终止线程,能做到线程取消自己吗?

2.1.4.2多接口使用
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include<cstdio>
#include <vector>

//取消线程自己
// 新线程执行函数
void *thread_routine(void *args)
{
    // 拿到参数
    std::string name = (const char *)args;
    // 新线程执行1秒就退出
    while (true)
    {
        std::cout << "我是新线程,ID:" << pthread_self() << std::endl;
        sleep(1);
        //线程自己取消自己
        pthread_cancel(pthread_self());
    }
}

int main()
{
    //新线程id
    pthread_t tid;
    // 创建线程
    pthread_create(&tid, nullptr, thread_routine, (void*)"thread-1");
    // 等待线程退出
    pthread_join(tid, nullptr);
    std::cout << "主线程退出" << std::endl;
    return 0;
}

但是这个方式只是提高我们使用接口的灵活性,这个方法来终止自己是不可取的,因为pthread_cancel本来就是为了取消其他线程而来的!

2.1.5线程的传参和返回值

我们现在有两个疑问:①pthread_join第二个参数是一个void**,二级指针类型,怎么理解?②新线程执行的函数的返回值为void*如何理解?

2.1.5.1编码层面

如果我们这样返回:

cpp 复制代码
return (void*)10;

10这个数字叫做字面值,它本身是有类型的是int占4个字节,因为它是字面值,所以它会硬编码到代码中,就和平时我们定义的字符串是一样的。

如果我们要获取返回值,因为pthread_join的第二个参数是输出型参数:

cpp 复制代码
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

因此,要带出来返回值就得这样:

cpp 复制代码
//获取返回值
void* ret;
// 等待线程退出
pthread_join(tid, &ret);
//得到返回值
//这样写会损失精度(64位下void*占8字节,int占4字节)
//(int) ret;
//这样写就行
(long long)ret;

因此,可以这样:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>

//获取线程返回值
// 新线程执行函数
void *thread_routine(void *args)
{
    // 新线程执行1秒就退出
    while (true)
    {
        sleep(1);
        break;
    }
    return (void*)10;
}

int main()
{
    //新线程id
    pthread_t tid;
    // 创建线程
    pthread_create(&tid, nullptr, thread_routine, (void*)"thread-1");
    //获取返回值
    void* ret;
    // 等待线程退出
    pthread_join(tid, &ret);
    //得到返回值
    //换为long long即可
    std::cout << "新线程的返回值:" << (long long)ret << std::endl;
    return 0;
}

如果我们用:

cpp 复制代码
pthread_exit((void*)20);

返回的话也能得到返回值为20!

因此我们可以让线程在不同地方返回不同的值代表这个线程把任务完成得怎么样!

2.1.5.2pthread_join第二个参数

那这个不对啊,我们进程退出的退出信息不应该是信号+退出码吗,难道多线程不考虑"异常"呢?

cpp 复制代码
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

这个接口可以等待新线程结束,那么获取新线程的执行结果,需要考虑新线程的异常问题吗?

不用考虑了,因为没机会。如果有一个线程内部出现问题:如除零野指针,那么整个进程都会结束,因此pthread_join不提供退出信号的参数!至于异常,这是父进程关心的,和新线程无关!

新线程如何把自己的返回值交给父进程的,父进程如何通过pthread_join获得指定子进程的退出信息?

当进程启动的时候,首先要做什么,需不需要把pthread库映射到内存里?

需要,先加载到内存里,然后通过页表映射到虚拟地址空间的共享区里,因此,程序启动时需要先加载对应的库!而我们之前说的FILE *fopen的FILE是一个结构体,FILE对象是fopen函数内部malloc出来的,因此,我们可以认为FILE对象是库申请的,就在库内部!----> 库可以帮我们维护结构体对象!

线程概念是谁提供的?

pthread库提供的!Linux并没有提供!而我们自己创建的线程需要管理起来,先描述,再组织!因此,就要求:需要描述一个线程的结构体,而这个结构体在pthread库内部!类似FILE对象!它在库里是struct tcb,不是内核提供的,它里面应当封装LWP和包含一个void* ret。

那么那个线程的ID是什么?

是虚拟地址,是结构体变量在库当中的起始地址!因此,当我们使用return或其他方法返回的void*,它应当存在在这个结构体变量内的变量ret里,因此,我们的join函数通过第一个参数线程ID找到对应的描述线程的描述结构体,然后通过查找找到对应的ret,返回给我们!

因此Linux的线程 ----- 用户级线程(我们用的是用户级别的pthread库来完成线程的管理工作),Windows的线程 ---- 内核级线程(内部封装了线程,实现了线程)。

2.1.5.3应用层面

通过刚刚的讲解,我们知道了:pthread_create的第一个参数返回的是线程控制块的起始地址,我们的void不能用来定义变量,因为它所占的字节数不固定,而void*所占的字节数是固定的,64位下为8字节!

没有任何人规定给线程传递的参数一定为字符串:

cpp 复制代码
pthread_create(&tid, nullptr, thread_routine, (void*)"thread-1");

我们这样写,第四个参数为字符串,实际上我们可以随意传,只需要保证最后是void*类型即可!返回值也同理,因此我们可以传递任意类型,甚至可以传递类对象!

如果想要得到对应的类型,直接强转即可:

cpp 复制代码
//类
class Task{};

// 新线程执行函数
void *thread_routine(void *args)
{
    Task* t = static_cast<Task*>(args);
    //对应的Task里面的方法
    return (void*)10;
}

因此,如果想要传很多的参,完全可以传递一个类对象,然后让这个线程执行对应的Task内部的方法,后面我们甚至也可以返回这个类对象!

2.1.6分离线程

2.1.6.1基本概念和使用

默认情况下,如果一个线程被创建出来,退出时需要进行pthread_join操作,因此说新创建的进程是joinable的,否则无法释放资源,导致内存泄漏,如果不关心线程的返回值,那么join就是一个负担,因此,我们可以告诉系统,当线程退出时,自动释放线程资源!

让线程进行分离线程,就可以做到自动释放线程资源:

cpp 复制代码
#include <pthread.h>
int pthread_detach(pthread_t thread);

因此线程的工作模式有两种:一种是joinable表示必须被join的,一种是detach的,线程必须是分离的,这两种工作模式自行选择!但是这两个模式都必须要保证主线程最后一个退出,否则会导致其他没运行完的线程出问题!

我们所有软件都是死循环,因此如果有了线程分离,主进程永远在死循环,只要来了一个任务主线程不用关心这个退出结果,创建一个线程让他去执行对应的任务即可,只要创建出来就分离。而且,每个线程内部还可以创建线程,因为线程本质上是进程创建的,任何线程都能创建线程!但不建议递归创建,因为会导致特别混乱,最好是统一创建,统一等待,统一管理!

线程分离本质是它还与其他线程共享资源,不会和进程分开,但是其最后的结果是什么样谁都不关心了!

线程分离可以是主线程提出进行线程分离,也可以由新线程提出和主线程进行分离,不管是哪个提出要分离,都可以调用这个接口!对于已经分离的线程,主线程不需要join等待分离的线程退出!

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>

// 新线程执行函数
void *thread_routine(void *args)
{
    // 新线程自己提出与主线程分离
    pthread_detach(pthread_self());
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        std::cout << "我是一个新线程" << std::endl;
        sleep(1);
        break;
    }
    return nullptr;
}

int main()
{
    // 新线程id
    pthread_t tid;
    // 创建线程
    pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
    // 让主线程等一下再等待
    sleep(1);
    void *ret;
    int n = pthread_join(tid, &ret);
    //如果打印出n为0就是join成功,否则失败
    std::cout << "等待成功:" << n << std::endl;
    return 0;
}

因为n的值为22不为0,所以代表join失败:

因此,我们可以加个报错信息提示:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include<cstring>

// 新线程执行函数
void *thread_routine(void *args)
{
    // 新线程自己提出与主线程分离
    pthread_detach(pthread_self());
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        std::cout << "我是一个新线程" << std::endl;
        sleep(1);
        break;
    }
    return nullptr;
}

int main()
{
    // 新线程id
    pthread_t tid;
    // 创建线程
    pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
    // 让主线程等一下再等待
    sleep(1);
    void *ret;
    int n = pthread_join(tid, &ret);
    //如果打印出n为0就是join成功,否则失败
    std::cout  << n << strerror(n) << std::endl;
    return 0;
}

Invalid argument:非法参数。

如果想让主线程提出与新线程分离:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include<cstring>

// 新线程执行函数
void *thread_routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        std::cout << "我是一个新线程" << std::endl;
        sleep(1);
        break;
    }
    return nullptr;
}

int main()
{
    // 新线程id
    pthread_t tid;
    // 创建线程
    pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
    // 让主线程与新线程分离
    pthread_detach(tid);
    void *ret;
    int n = pthread_join(tid, &ret);
    //如果打印出n为0就是join成功,否则失败
    std::cout  << n << strerror(n) << std::endl;
    return 0;
}

我们能看到报错,代表线程分离成功:

因此,要把线程分离最佳实践应该是:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include<cstring>

//线程分离最佳实践
// 新线程执行函数
void *thread_routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        std::cout << "我是一个新线程" << std::endl;
        sleep(1);
        break;
    }
    return nullptr;
}

int main()
{
    // 新线程id
    pthread_t tid;
    // 创建线程
    pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
    // 让主线程与新线程分离
    pthread_detach(tid);
    //然后主线程一直死循环
    while(true);
    return 0;
}

因为我们没必要join,而且主线程应当一直死循环遇到任务就创建一个线程!

2.1.6.2返回值问题

那么如果我们主动取消这个线程呢,对应返回值该为多少,退出信息?

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>

// 新线程执行函数
void *thread_routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        std::cout << "我是一个新线程" << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    // 新线程id
    pthread_t tid;
    // 创建线程
    pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
    //取消线程
    sleep(2);
    pthread_cancel(tid);
    //等待线程
    void* ret = nullptr;
    int n = pthread_join(tid,&ret);
    std::cout << "返回值:" << n << "退出信息:" << (long long)ret << std::endl;
    return 0;
}

因此,线程被取消是-1,实际上它是一个宏值,宏:PTHREAD_CANCELED。

2.2C++的线程

C++里面也有线程,C++里面有thread函数,有thread库,因此,需要使用C++的线程就需要使用C++的库:thread

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <thread>

//线程执行的方法
void fun()
{
    while(true)
    {
        std::cout << "我是一个新线程" << std::endl;
        sleep(1);
    }
}

int main()
{
    //创建一个线程
    std::thread t(fun);
    //等待线程
    t.join();
    return 0;
}

在Linux中,C++的多线程本质是对pthread库做封装!

代码能运行:

C++为什么要支持多线程?因为其他语言也有,所以就必须要设计出来!

C++如何保证自己的多线程被更多人使用?因此需要C++设计出跨平台性!一种平台,背后就是一批用户,跨平台之争,本质就是用户之争!

C++如何做到跨平台性?每个平台的多线程封装,都做一份!(所有语言都是这样做的)

为什么一个语言要支持一个新特性,尤其是如果与平台强相关,支持跨平台性就非常难!

推荐使用C++的多线程,但是里面还有一些东西是我们没学过的,这就需要我们自己更加深入了解pthread库!

pthread库是共享库,因此,线程管理并不是以进程为单位的,而是全部系统的LWP最后都由pthread库管理。因此,在C语言中,malloc开辟的空间哪怕没有free,最后还是会释放的,因为其他进程也会malloc空间,因此C标准库会把每个进程malloc的空间统一管理起来,会用链表结构来管理起来,如果你的进程结束了,堆空间的引用计数变为0,别的进程free时就会顺便释放你申请的空间!因此我们完全不用担心堆空间没有被释放!

2.3线程局部存储

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <cstdio>

//线程局部存储
int gval = 0;

//转化地址为16进制
std::string ToHex(void* addr)
{
    char addrbuffer[64];
    snprintf(addrbuffer,64,"0x%p",addr);
    return addrbuffer;
}

// 新线程执行函数
void *thread_routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        std::cout << "我是一个新线程,名字:" << name << ",gval:" << gval << ",&gval:" << ToHex(&gval) << std::endl;
        //新线程对全局变量修改
        gval++;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    // 新线程id
    pthread_t tid;
    // 创建线程
    pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
    sleep(2);
    //死循环
    while(true)
    {
        std::cout << "我是一个主线程,gval:" << gval << ",&gval:" << ToHex(&gval) << std::endl;
        sleep(1);
    }
    return 0;
}

有:

这个就代表每个线程都能看到全局变量!

如果线程想要自己的全局变量,只想要这个全局变量自己看到。

因此,可以把全局变量改为这样的定义:

cpp 复制代码
__thread int gval = 0;

其他的代码不变的情况下就能做到每个线程都有自己的"全局变量"了:

这个gval在语法上是全局的,这个变量有两个,且地址不一样,即它们用到了不同的虚拟地址!

__thread:gcc/g++提供的内置选项,修饰某个变量,让它成为每一个线程的局部存储!

因此,这就叫做线程局部存储:给每个线程都存储一份变量!

线程局部存储往往是用来保存内置类型的数据,结构体和类这种类型是做不到的!

对应的代码已经上传到Gitee上,感兴趣的可以去查看:

thread · 谭旦0000/test_for_csdn - 码云 - 开源中国

相关推荐
咖丨喱1 小时前
【P2P 邀请后 5G 单信道扫描 dwell 加长】
服务器·5g·asp.net
Decay__1 小时前
03_MySQL 存储引擎与存储管理 —— InnoDB 深入、表空间/ibd、binlog/redo/undo 与双写缓冲
运维·数据库·sql·mysql·架构
Z21-X1 小时前
云原生运维笔记:打通OpenStack的任督二脉——从Neutron网络踩坑到Swift与Heat硬核实战
运维·笔记·学习·云原生
C++ 老炮儿的技术栈1 小时前
Linux 动态库踩坑:明明重新编译了 .so,调用函数返回值却一直不变(libfjrobot.so 版本号异常排查实录)
linux·c语言·网络·c++·visual studio
你看我名字2 小时前
云计算工程师技能树:从 Linux 基础到云平台架构能力
linux·架构·云计算
广州灵眸科技有限公司9 小时前
4G 模组开机自连 systemd 服务方案:基于灵眸科技EAI1106--Core-T
运维·服务器·科技
天远数科10 小时前
零信任架构实战:基于天远企业股权变更构建自动化商业银行对公授信存续期股权异动与资产抽逃预警网关
运维·人工智能·架构·自动化
qq_4017004110 小时前
Linux设备树详解
linux·运维·服务器
云梦谭11 小时前
让 Linux 主动连接 Windows:配置 Windows 11 SSH 服务端
linux·windows·ssh