Linux_14(多线程)线程控制+C++多线程

1.线程控制

上一篇讲了线程/轻量级进程的概念,这篇讲讲线程的控制:退出,等待......

1.1 线程创建(pthread_create)

线程创建上一篇已经讲过了:

创建线程使用到的库函数接口,man pthread_create

  • pthread_t *thread:线程标识符tid,是一个输出型参数。
  • const pthread_attr_t* attr:线程属性,当前阶段一律设成nullptr。
  • void* (*start_routine)(void *):是一个函数指针,线程执行的就是该函数中的代码。
  • void* arg:传给线程启动函数的参数,是上面函数指针指向函数的形参。
  • 返回值:线程创建成功返回0,失败返回错误码。

看一下上篇出现线程异常的场景:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
 
void *threadRun(void *args)
{
    const string name = (char *)args;
    while (true)
    {
        cout << name << ", pid: " << getpid() << endl;
        sleep(1);
        static int cnt = 0;
        if (cnt++ == 7)
        {
            int *p = nullptr;
            *p = 777;
        }
    }
}
 
int main()
{
    pthread_t tid[5];
    char name[64];
    for (int i = 0; i < 5; i++)
    {
        snprintf(name, sizeof(name), "%s-%d", "thread", i); // 特定内容格式化到name中
        pthread_create(tid + i, nullptr, threadRun, (void *)name);
        sleep(1); // 缓解传参的bug
    }
 
    while (true)
    {
        cout << "main thread, pid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

出现异常全部线程都退出了,有没有办法只让其中一个线程退出?

试试exit();

还是全部线程都退出了,这也再次认识了 exit(); 就是终止进程的,所以不建议用


1.2 线程结束(pthread_exit)

基于前面,threadRun函数还有个是函数指针的返回值,返回个空指针试试:

可以发现名字为thread-1的线程好像退出了,其它线程没退出,就创建一个新线程看看:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
 
void *threadRun(void *args)
{
    const string name = (char *)args;
    while (true)
    {
        cout << name << ", pid: " << getpid() << endl;
        sleep(1);
        static int cnt = 0;
        if (cnt++ == 7)
        {
            // int *p = nullptr;
            // *p = 777;
            // exit(10);
            return nullptr;
        }
    }
}
 
int main()
{
    // pthread_t tid[5];
    // char name[64];
    // for (int i = 0; i < 5; i++)
    // {
    //     snprintf(name, sizeof(name), "%s-%d", "thread", i); // 特定内容格式化到name中
    //     pthread_create(tid + i, nullptr, threadRun, (void *)name);
    //     sleep(1); // 缓解传参的bug
    // }
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
 
    while (true)
    {
        cout << "main thread, pid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

可以发现新线程退出了,主线程还没退出。


POSIX线程库专门提供了一个接口来结束线程:pthread_exit()结束线程:man pthread_exit

  • void* retval:返回线程结束信息,当前阶段设置成nullptr即可。

调用该接口的线程会结束。


cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
 
void *threadRun(void *args)
{
    const string name = (char *)args;
    while (true)
    {
        cout << name << ", pid: " << getpid() << endl;
        sleep(1);
        static int cnt = 0;
        if (cnt++ == 7)
        {
            // int *p = nullptr;
            // *p = 777;
            // exit(10);
 
            //return nullptr;
            pthread_exit(nullptr);
        }
    }
}
 
int main()
{
    // pthread_t tid[5];
    // char name[64];
    // for (int i = 0; i < 5; i++)
    // {
    //     snprintf(name, sizeof(name), "%s-%d", "thread", i); // 特定内容格式化到name中
    //     pthread_create(tid + i, nullptr, threadRun, (void *)name);
    //     sleep(1); // 缓解传参的bug
    // }
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
 
    while (true)
    {
        cout << "main thread, pid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

同样,7秒后,新线程会调用该接口,然后就只剩下主线程了,新线程结束了。


1.3 线程等待(pthread_join)

和进程一样,线程也是需要等待的,如果不等待会造成内存泄漏,也就是结束掉的线程PCB不会被回收(类似僵尸进程),但是我们看不到没有回收的现象。

线程等待系统调用:

  • 第一个参数pthread_t thread:要等待的线程tid。
  • 第二个参数void** retval:线程结束的信息返回,这是一个输出型参数。
  • 返回值:等待成功返回0,等待失败返回错误码。

演示一下使用:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
 
void *threadRun(void *args)
{
    const string name = (char *)args;
    while (true)
    {
        cout << name << ", pid: " << getpid() << endl;
        sleep(1);
        static int cnt = 0;
        if (cnt++ == 7)
        {
            pthread_exit((void*)777);
        }
    }
}
 
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
 
    int *ret = nullptr;
    pthread_join(tid, (void **)&ret); // 默认会阻塞等待新线程退出
    cout << "main quit ...: new thead quit : " << (long long)ret << endl;
    // linux下64位的,指针是8个字节,所以强转成long long 8个字节
    while (true)
    {
        cout << "main thread, pid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

可以看到,主线程在执行到线程等待的时候,会阻塞等待,不再往下执行,直到新线程都等待成功才会继续向下执行。

在主线程的栈区中有一个void类型的指针变量,新线程中返回的void类型指针会放到这个ret中。

  • pthread线程库中有一个void** 类型的二级指针变量retval。
  • pthread_join()系统调用将主线程中void*类型的指针变量的地址传给了pthread线程库中的二级指针变量,此时主线程就和线程库建立了联系。
  • 将新线程中返回到线程库中的void*指针变量中的返回值,通过这种联系放到主线程中指针变量中----也就是 *retval = ret。

这样,我们就可以成功的获取到新线程退出时的返回信息了,桥梁就是pthread_join()系统调用。


在学习进程等待的时候,我们不仅可以获得进程的退出信息,还能获得进程的退出信号,但是在线程退出时就没有获得线程退出信号,这是为什么呢?

因为信号是发给进程的,整个进程都会被退出,线程要退出信号也没有意义了。

而且pthread_join默认是能够等待成功的,并不考虑异常的问题,异常是进程要考虑的事,线程不用考虑。


1.4 线程取消(pthread_cancel)

Linux提供了线程结束的其它方式:线程取消,线程取消的接口:

  • 参数:要取消的线程tid。
  • 返回值:取消成功返回0,失败返回错误码。
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
 
void *threadRun(void *args)
{
    const string name = (char *)args;
    while (true)
    {
        cout << name << ", pid: " << getpid() << endl;
        sleep(1);
    }
}
 
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
    int cnt = 0;
    while (true)
    {
        cout << "main thread, pid: " << getpid() << endl;
        sleep(1);
        if(cnt++ == 5)
        {
            break;
        }
    }
 
    pthread_cancel(tid);
    cout << "pthread cancel: " << tid << endl;
 
    int *ret = nullptr;
    pthread_join(tid, (void **)&ret); // 默认会阻塞等待新线程退出
    cout << "main quit ...: new thead quit : " << (long long)ret << endl;
    // linux下64位的,指针是8个字节,所以强转成long long 8个字节
    return 0;
}

可以看见,如果一个线程是被取消结束的,它的退出码就是-1。它其实是一个宏定义:#defin PTHREAD_CANCELED -1。

线程取消也是一种线程结束的方式,放在这里是为了能够通过线程等待看线程退出的退出码。


1.5 线程tid (pthread_self())

有没有看见退出得到的tid是一个很大的整数?这个整数实际上是一个地址。

我们还可以通过系统接口pthread_self在上面代码基础上打印自己的tid:

tid的值是一个地址:

我们知道,Linux内核中是没有线程概念的,也没有对应的TCB结构。

  • 用户创建线程时使用的是POSIX线程库提供的接口。
  • 线程库中会调用clone()系统调用接口,在内核中创建线程复用的PCB结构。
  • 这些轻量级进程共用一个进程地址空间。

系统中肯定不只一个线程存在,大量的线程势必要管理起来,管理的方式同样是先描述再组织 。既然Linux内核中只有轻量级进程的PCB,那么描述线程的TCB结构就只能存在于线程库中

线程库中的TCB里,存放着线程的属性,这里的TCB被叫做用户级线程

Linux线程方案:用户级线程和用户关心的线程属性都在线程库中,内核提供线程执行流的调度。

一个线程的所有属性描述是由两部组成的,一部分就是在pthread线程库中的用户级线程,另一部分就是Linux中的轻量级进程,它们俩的比例大约是1比1。

pthread线程库从磁盘上加载到内存中后,通过页表再将虚拟地址空间和物理地址映射起来。

线程库最终是映射在虚拟地址空间中的共享区中的mmap区域。

线程库是映射在共享区的,那么线程库所维护的TCB结构也就一定在共享区。

如上图所示,将映射到共享区的动态线程库放大。

线程库中存在多个TCB结构来描述线程。每个TCB的地址就是线程id。

线程tid的本质就是虚拟地址共享区中TCB结构体的地址

线程的栈也在共享区中,而不在栈中。

虚拟地址空间中的栈是主线程的栈,共享区中动态库中的栈是新线程的栈。

所以说,线程的栈结构是相互独立的,因为存在于不同的TCB中(主线程除外)


1.6 线程局部存储(__thread)

在共享区线程库中的TCB里,有一个线程的局部存储属性,它是一个介于全局变量和局部变量之间线程特有的属性。

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
 
int g_val = 0;
 
void *threadRun(void *args)
{
    const string name = (char *)args;
    while (true)
    {
        //cout << name << ", pid: " << getpid() << " tid: " << pthread_self() << endl;
        cout << name << " -> g_val: " << g_val++ << " &g_val: " << &g_val << endl;
        sleep(1);
    }
}
 
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
    int cnt = 0;
    while (true)
    {
        cout << "main thread -> g_val: " << g_val++ << " &g_val: " << &g_val << endl;
        sleep(1);
        if(cnt++ == 5)
        {
            break;
        }
    }
 
    pthread_cancel(tid);
    cout << "pthread cancel: " << tid << endl;
 
    int *ret = nullptr;
    pthread_join(tid, (void **)&ret); // 默认会阻塞等待新线程退出
    cout << "main quit ...: new thead quit : " << (long long)ret << endl;
    // linux下64位的,指针是8个字节,所以强转成long long 8个字节
    return 0;
}

主线程和新线程打印的地址都是一样的,说明主线程和新线程共用一个全局变量。

那如果此时新线程仍然想用这个变量名,但是又不想影响其他线程,也就是让这个全局变量独立出来,该怎么办呢?此时就可以使用线程的局部存储属性了。

全局变量 g_val前面加__thread (两个下划线),此时这个全局变量就具有了局部存储的属性。

主线程和新线程打印出来的全局变量的地址不相同了,说明此时用的并不是同一个全局变量。

而且新线程修改这个值,主线程不受影响。

将全局变量或者static变量添加 __thread,设置位线程局部存储。

此时每个线程的TCB中都会有一份该变量,相互独立,并不会互相影响。


1.7 线程分离(pthread_detach)

前面线程等待的时候,主线程就需要阻塞式等待线程的释放,主线程什么都干不了。能不能像进程那样不需要阻塞式等待(将SIGCHID信号设置为忽略),等新线程结束以后自动释放呢?(尤其是不需要关心线程返回值的时候,join是一种负担。)

当然可以,将需要自动释放的线程设置成分离状态,将线程设置成分离状态意味着不需要主线程再关心该线程的状态,它会自动释放。

线程分离的接口:man pthread_detach:

  • 参数 pthread_t thread:要分离的线程tid。
  • 返回值 int:成功返回0,不成功返回错误码。

可以是线程组内其他线程对目标线程进行分离,但一般是线程自己分离自己:

cpp 复制代码
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
using namespace std;
 
__thread int g_val = 0;
 
void *threadRun(void *args)
{
    pthread_detach(pthread_self());
 
    const string name = (char *)args;
    while (true)
    {
        //cout << name << ", pid: " << getpid() << " tid: " << pthread_self() << endl;
        cout << name << " -> g_val: " << g_val++ << " &g_val: " << &g_val << endl;
        sleep(5);
        pthread_exit((void*)777);
    }
}
 
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
    int cnt = 0;
    while (true)
    {
        cout << "main thread -> g_val: " << g_val++ << " &g_val: " << &g_val << endl;
        sleep(1);
        if(cnt++ == 5)
        {
            break;
        }
    }
 
    int *ret = nullptr; // 新线程自己分离了,但是主线程非要等待呢?
    int n = pthread_join(tid, (void **)&ret); // 默认会阻塞等待新线程退出
    if(n == 0)
    {
        cout << "main quit ...: new thead quit : " << (long long)ret << endl;
    }
    else
    {
        cout << "n :" << n << "errstring: " << strerror(n) << endl;
    }
    // linux下64位的,指针是8个字节,所以强转成long long 8个字节
    return 0;
}

可以看到,此时主线程在进行线程等待的时候就会失败,而且返回错误码。


2. C++的多线程

C++也是可以多线程编程的,而且提供了多线程的库,而无论什么编程语言,什么库,在Linux系统上的多线程本质上都是对pthread原生线程库的封装

简单演示一下:

Makefile:

cpp 复制代码
mythread:mythread.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f mythread

mythread.cc

cpp 复制代码
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;
 
void fun()
{
    while(true)
    {
        cout << "hello new thread" << endl;
        sleep(1);
    }
}
 
int main()
{
    std::thread t(fun);
    std::thread t1(fun);
    std::thread t2(fun);
    std::thread t3(fun);
    std::thread t4(fun);
 
    while(true)
    {
        cout << "hello main thread" << endl;
        sleep(1);
    }
 
    t.join();
}

此时就发现运行不了了,改下Makefile:

cpp 复制代码
mythread:mythread.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f mythread

此时程序就能正常运行了,演示这个主要为了说明无论什么编程语言,什么库,在Linux系统上的多线程本质上都是对pthread原生线程库的封装


本章完

下一章:Linux_15(多线程)线程安全+线程互斥(加锁)+死锁-CSDN博客

相关推荐
Scott9999HH4 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔4 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海5 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区8 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大8 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai9 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说9 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json