
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[一、进程 VS 线程:资源共享与私有划分](#一、进程 VS 线程:资源共享与私有划分)
[1.1 核心定位再梳理](#1.1 核心定位再梳理)
[1.2 线程间共享的进程资源](#1.2 线程间共享的进程资源)
[1.3 线程私有的独立资源(重点:涉及面试考点)](#1.3 线程私有的独立资源(重点:涉及面试考点))
[二、Linux 线程控制:从创建到回收全流程](#二、Linux 线程控制:从创建到回收全流程)
[2.1 前置说明:pthread 线程库](#2.1 前置说明:pthread 线程库)
[2.1.1 原生线程库](#2.1.1 原生线程库)
[2.1.2 pthread库](#2.1.2 pthread库)
[2.1.3 原生线程库和pthread库之间关系](#2.1.3 原生线程库和pthread库之间关系)
[2.1.4 Linux线程的验证](#2.1.4 Linux线程的验证)
[2.1.5 修改代码:验证线程的健壮性低,缺乏访问控制](#2.1.5 修改代码:验证线程的健壮性低,缺乏访问控制)
[2.1.6 C++11的多线程创建](#2.1.6 C++11的多线程创建)
[2.2 线程创建:pthread_create](#2.2 线程创建:pthread_create)
[2.2.1 参数](#2.2.1 参数)
[2.2.2 返回值](#2.2.2 返回值)
[2.3 线程等待:pthread_join](#2.3 线程等待:pthread_join)
[2.4 pthread_self](#2.4 pthread_self)
[2.5 获取线程ID](#2.5 获取线程ID)
[2.6 线程的传参和返回值(pthread_join)](#2.6 线程的传参和返回值(pthread_join))
[2.6.1 示例1:传参为字符串,返回值为一个数字](#2.6.1 示例1:传参为字符串,返回值为一个数字)
[2.6.2 深度逻辑:为什么参数是二级指针 void **?](#2.6.2 深度逻辑:为什么参数是二级指针 void **?)
[2.6.3 构建任务:可以传递C++中的类对象](#2.6.3 构建任务:可以传递C++中的类对象)
[2.7 线程终止:pthread_exit && pthread_cancle](#2.7 线程终止:pthread_exit && pthread_cancle)
[2.7.1 pthread_exit函数:线程主动终止自身](#2.7.1 pthread_exit函数:线程主动终止自身)
[2.7.2 pthread_cancel函数:终止同进程的其他线程](#2.7.2 pthread_cancel函数:终止同进程的其他线程)
[2.8 线程分离:pthread_detach](#2.8 线程分离:pthread_detach)
[2.9 创建多线程](#2.9 创建多线程)
[2.9.1 传参在栈上开辟空间(错误做法)](#2.9.1 传参在栈上开辟空间(错误做法))
[2.9.2 传参在堆上开辟空间(正确做法)](#2.9.2 传参在堆上开辟空间(正确做法))
前言
上篇文章是我们第一次接触线程这个概念,让我们清楚了:进程是操作系统资源分配的基本单位;线程是 CPU 调度的最小单位 ,是进程内部的执行分支 ,而在 Linux 下本质是轻量级进程(LWP) ,除此之外我们还讲解了一个非常重要的知识点------分页机制 。在最后我们对线程的优缺点 进行了阐述,但是对于线程的缺点我们并没有加以证明。
本篇文章的主要内容是:进程 VS 线程资源划分 + Linux 线程控制,包含了资源共享 / 独占的底层逻辑,到线程创建、终止、等待、分离,在其中我们就会穿插使用讲解的代码来证明上篇文章中留下的问题。
如果想要真正用好线程,必须先搞懂两个核心问题:
- 同进程内的线程,哪些资源共享?哪些资源私有?
- 如何通过系统接口,完成线程的全生命周期控制?
一、进程 VS 线程:资源共享与私有划分
线程的 "轻量化",本质是共享了进程的绝大部分资源,仅私有运行必需的最小上下文。我们先把共享与私有资源划清,这是理解线程所有特性的基础。
1.1 核心定位再梳理
- 进程 = 内核数据结构(task_struct/mm_struct 等) + 私有的代码和数据,承担系统资源分配
- 线程 = 进程内部的执行分支,粒度更细、更轻,仅需维护自身运行上下文
- 内核通过 task_struct 统一管理进程和线程,线程与进程的核心差异 ,就在于资源是否共享。
进程和线程的关系:

1.2 线程间共享的进程资源
同进程内的所有线程,共享进程地址空间内的绝大部分资源 ,无需额外申请,这也是线程创建 / 切换成本极低的核心原因:
- 地址空间核心段:代码段 (Text Segment)、数据段 (Data Segment)、堆区 、共享区
- 全局变量 、静态变量 、堆内申请的内存,所有线程都可直接访问修改
- 文件系统相关:文件描述符表 、当前工作目录 、用户 id / 组 id
- 一个线程打开的文件,其他线程可直接读写
- 信号相关:每种信号的处理方式 (SIG_IGN/SIG_DFL/ 自定义处理函数)
- 信号是发给进程的,任意线程收到信号,都会触发整个进程的信号处理函数
- 进程内核数据:页表 、mm_struct 内存描述符 、vm_area_struct 内存区域结构
1.3 线程私有的独立资源(重点:涉及面试考点)
虽然同一个进程中的所有线程,进程地址空间内绝大部分资源都是共享的,但重点不在于共享的那部分资源,而是究竟哪些资源是线程所私有独占的。
线程要独立被 CPU 调度执行 ,必须私有运行时的上下文数据 ,这些资源线程间完全隔离,互不影响:
- 线程 ID(TID):用户态 pthread 库的唯一标识,内核态对应 LWP 号
- 寄存器上下文:CPU 寄存器的值,线程切换时保存 / 恢复,保证执行流不混乱
- 独立栈空间:每个线程有自己的私有栈,存放局部变量、函数调用栈帧,互不干扰
- errno 变量:系统调用错误码,线程私有,避免多线程间错误码互相覆盖
- 信号屏蔽字:每个线程可独立设置信号屏蔽规则,不影响其他线程
- 调度优先级:线程可独立设置调度优先级,由内核单独调度
- 线程局部存储(TLS):线程私有的全局变量,仅当前线程可访问
面试问题:线程相比于进程,线程有哪些资源是独占的?
其实上面就是问题的答案,但是其中有两点是非常重要必须回答的:一组寄存器,线程的上下文;独立栈空间**。**
线程切换 要对上下文进行保护 ,所以上下文数据必然是私有 的,线程有自己独立的上下文结构。
调用函数 就要在栈创建栈帧 ,所以一定要有自己独立的栈结构!每个线程如何拥有独立的栈结构呢?后面会介绍!
如果在面试的时候问到了这个问题,把这两点答出来,面试官会认为这个人对线程是有理解的!对线程要有个动态概念,因为要切换所以是私有线程上下文的,因为要自己执行,所以要有自己的独立的栈结构的。
整体图示解析:

二、Linux 线程控制:从创建到回收全流程
Linux 内核仅提供轻量级进程的创建能力 ,我们日常使用的线程接口,都来自POSIX 标准的 pthread 线程库 ,所有接口都以pthread_ 开头,在编译时需要加上**-lpthread**链接线程库。
2.1 前置说明:pthread 线程库
- 头文件:必须包含**<pthread.h>**
- 编译命令**:gcc xxx.c -o xxx -lpthread**
- 核心特点:线程库运行在用户态,负责线程的管理(TCB 线程控制块、线程栈分配),底层通过clone系统调用让内核创建 LWP(其实我们之前学习的fork也是这样的)。
2.1.1 原生线程库


2.1.2 pthread库

2.1.3 原生线程库和pthread库之间关系
通过上面对两者的简单介绍,我们不难发现:
如果用一句话总结:pthread库 是**"说明书"(接口标准)** ,原生线程库 是"按照说明书造出来的具体机器(具体实现)。

说的再简单一点就是:不同操作系统 pthread库 东西是一样的,但 原生线程库 是不同的。


2.1.4 Linux线程的验证
在详细讲解pthread线程库中的相关接口前,我们先用其中一个创建线程的接口pthread_create验证一下,在这里先不详细对这个接口进行讲解,后续再提。
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
void *threadrun(void *args)
{
// pthread_create最后一个参数会传给函数参数args
std::string name = (const char *)args;
while (true)
{
std::cout << "我是新线程, name: " << name << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid; // 线程id
pthread_create(&tid, nullptr, threadrun, (void *)"thread--1");
while (true)
{
std::cout << "我是主线程......" << std::endl;
sleep(1);
}
return 0;
}
上面就是最简单的创建线程的方式,我们对其进行编译:

我们就发现了如果创建线程,编译时如果没有加上**-lpthread 选项**编译就无法成功。
但真的是一定要加上 -lpthread 选项 吗?其实并不是。
而之所以上面情况不加 -lpthread 选项 导致编译不通过原因是glibc的版本比较低引起的。
而对于较新版本的 glibc 不加 -lpthread 选项 也可以成功编译通过:


那这里肯定就有人提问了:现在我们使用的都是新版本的glibc,那为什么我们还要"多此一举"把 -lpthread 选项 带上呢?
这个变化,背后其实是glibc 架构 的一次重大演进,以及对**"现代并发"语义的重新定义**。

因此,"老的glibc要带上 -l pthread,新的即使不需要带 -l pthread 也能够编译,但是我们还是带上-l pthread。"的做法是最佳实践!
运行:
- 系统中查看进程只能查到一个

进程的确只有一个,那怎么证明就是存在两个线程呢?
- ps -aL

我们会发现其中只有一个东西的值是不一样的 ------LWP ,也就是我们的light weight process(轻量级进程)。而其中一个LWP和PID的值相同,也就是我们的主线程了,2285对应的也就是我们新创建的线程。
那就出现一个问题了:线程也需要被CPU进行调度,而多个线程就需要进行线程切换,那CPU调度的时候,看的到底是PID还是LWP呢?
如果是利用之前的知识来回答我们会认为是PID,但其实是错的,因为我们现在知道了多个线程可能存在于同一个进程当中,那么PID都是相同的,CPU怎么通过PID来进行线程调度呢?所以CPU调度的时候看的是LWP !
只不过之前我们讲进程的时候都默认的是单线程进程,也就只有一个主线程,PID和LWP的值正好相同而言,所以我们会误认为CPU调度的时候看的是PID。

2.1.5 修改代码:验证线程的健壮性低,缺乏访问控制
在上篇文章我们讲解了线程的优缺点,其中谈到了线程有一个很大的缺点就是:健壮性低,缺乏访问控制。但我们只是提到了这个缺点没有进行代码证明,现在我们通过代码加以证明。
**健壮性降低:**一个线程异常,全进程内部的线程遭殃,整个进程出问题!
我们故意让一个线程出异常:
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
void *threadrun(void *args)
{
// pthread_create最后一个参数会传给函数参数args
std::string name = (const char *)args;
while (true)
{
std::cout << "我是新线程, name: " << name << ", pid: " << getpid() << std::endl;
sleep(1);
//线程异常
int a = 10;
a /= 0; //除零异常
//任何一个线程崩溃,都会导致整个进程全部崩溃
}
return nullptr;
}
int main()
{
pthread_t tid; // 线程id
pthread_create(&tid, nullptr, threadrun, (void *)"thread--1");
while (true)
{
std::cout << "我是主线程......" << ", pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}

为什么?因为线程是进程内部的执行分支,线程在执行这个工作的时候,线程除0就相当于进程除0,都属于一个团队内部,你线程就是在替进程做事的。
一个线程出现异常的本质是什么?是收到信号 (当然这之间还有中断的事)------OS给你这个LWP发送的。信号是以进程为单位为载体发送的,你可以理解给进程内的所有LWP都发了,每个线程的pending位图都修改了,所以每个线程执行处理方法(这个是共享的)时,整个都在执行。所以整个进程都退出。


**缺乏访问控制:**多线程对于大部分资源都是共享的(能够访问到的资源大部分都是共享的)

我们发现有时候打印时多个线程会导致打印结果错乱,这是为什么呢?

而对于全局变量,由于所有的线程(多线程)共享同一个虚拟地址空间,所以采用同一个全局变量:
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
int g_val = 100;
void *threadrun(void *args)
{
// pthread_create最后一个参数会传给函数参数args
std::string name = (const char *)args;
while (true)
{
std::cout << "我是新线程, name: " << name << ", pid: " << getpid() << ", g_val: " << g_val << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid; // 线程id
pthread_create(&tid, nullptr, threadrun, (void *)"thread--1");
while (true)
{
std::cout << "我是主线程......" << ", pid: " << getpid() << std::endl;
sleep(1);
g_val++; //主线程对全局变量进行++修改操作
}
return 0;
}

证明了所有的线程(多线程)共享同一个虚拟地址空间,采用同一个全局变量------共享全局变量。
2.1.6 C++11的多线程创建
上面我们一直讲的是在Linux下创建线程,也就是包含**<pthread.h>** ,那C++能不能创建线程呢,也就是不包含**<pthread.h>**?其实是可以的,C++也提供了线程。
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <thread>
void Hello()
{
while (true)
{
std::cout << "我是新线程, pid: " << getpid() << std::endl;
sleep(1);
}
}
int main()
{
std::thread t(Hello);
while (true)
{
std::cout << "我是主线程..., pid: " << getpid() << std::endl;
sleep(1);
}
t.join();
return 0;
}
C++11里面的多线程和Linux中的pthread库有什么关系?
我们先对上面的代码进行编译,因为我们是使用C++11创建的线程,使用并没有使用pthread库里面的函数,所以我们就把 -lphread 选项去掉:

我们会惊讶的发现即使我们使用C++11创建线程,没有加上 -lphread 选项依旧无法编译,而且报错原因竟然和上面linux创建线程时不加 -lphread 选项非常类似。

C++中的线程库也是对pthread库(C语言,无法做到面向对象)做了进一步地封装,面向对象的封装,就可以面向对象了。
自此我们就能得到一个结论:

总结:

2.2 线程创建:pthread_create
用于创建一个新的用户态线程,是线程控制最基础的接口。
函数原型:

2.2.1 参数
| 参数 | 作用说明 |
|---|---|
| thread | 输出型参数,用于返回新创建线程的用户态线程 ID |
| attr | 线程属性设置 ,传NULL 表示使用默认属性(栈大小、调度策略等) |
| start_routine | 函数指针 ,线程的入口执行函数,线程创建成功后,会从这个函数开始执行 |
| arg | 传递给线程入口函数的参数 |
(1)第一个参数是一个输出型参数,用户层面的线程ID,这个ID有说法啊(但是这个可不是LWP),我们后面来研究。
(2)第二个基本上都是NULL。
(3)第三个参数是想让你的线程执行那一个方法。
(4)最后一个参数是传递给第三个的,回调。
2.2.2 返回值
-
成功:返回 0
-
失败:直接返回错误码(不会设置 errno),无需通过 perror 打印,用**strerror(ret)**解析错误信息

2.3 线程等待:pthread_join
为什么需要线程等待?
-
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
-
创建新的线程不会复用刚才退出线程的地址空间。
线程退出后,其资源不会被自动释放,必须通过pthread_join 等待线程退出 ,回收资源,否则会造成内存泄漏。
函数原型:
cpp
int pthread_join(pthread_t thread, void **value_ptr);
参数详解:
- thread:要等待的目标线程 ID;
- value_ptr:输出型参数,用于接收线程的退出返回值。如果不关心退出码,传 NULL 即可。
返回值
- 成功:返回 0
- 失败:返回错误码
线程退出码的 3 种情况(后面会一一讲解):
- 线程通过return 返回:value_ptr 指向的是线程 return 的返回值;
- 线程通过pthread_exit 终止:value_ptr 指向的是 pthread_exit 传入的参数;
- 线程被 pthread_cancel 取消:value_ptr 指向的是常数 PTHREAD_CANCELED。
2.4 pthread_self
- 获取调用 pthread 自己的 id

2.5 获取线程ID
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
void showtid(pthread_t &tid)
{
//typedef unsigned long int pthread_t; -> %ld
printf("新线程tid: %ld\n", tid);
}
std::string FormatId(pthread_t &tid)
{
char id[64];
snprintf(id, sizeof(id), "%ld", tid);
return id;
}
void *routine(void *args)
{
std::string name = static_cast<const char *>(args);
int cnt = 5;
// 证明pthread_create输出的id就是新线程的id->pthread_self
pthread_t tid = pthread_self();
while (cnt--)
{
std::cout << "我是新线程, 我的名字是: " << name << ", 我的ID是: " << FormatId(tid) << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void *)"thread--1");
// pthread_create获取到线程id,进行打印
showtid(tid);
int cnt = 5;
// 新线程有自己的id,main自然也有自己的线程id
pthread_t mtid = pthread_self();
while (cnt--)
{
std::cout << "我是main线程, 我的ID是: " << FormatId(mtid) << std::endl;
sleep(1);
}
// 线程等待pthread_join
pthread_join(tid, nullptr);
return 0;
}

我们会发现获取到的线程ID和前面讲解的LWP的值完全不一样,而且线程ID的值看起来非常的大。这是为什么呢?
原因很简单:因为线程ID 这个东西本来就属于线程库封装 的,就不想在暴露轻量级进程里的LWP了,所以不同是正常的,但是这两玩意肯定有关系。
线程ID是pthread库封装的------不想暴露更多细节。
线程ID不一样是正常的,相当于做了隐藏。
线程ID不是LWP,那线程ID是什么呢?我们现阶段还没办法详细说明。
- 不过我们换种方式打印一下我们可以见一见,一会儿可以先直接出个结论------我们以%p格式打印一下看看(%p打印一下------十六进制,看着顺眼多了。)


好了,已经够了,我们可以得出结论了------关于线程ID的本质:
- 线程ID的本质其实是一个虚拟地址!
线程ID是什么现阶段我们还不关心,后面我们仔细说。
2.6 线程的传参和返回值(pthread_join)
2.6.1 示例1:传参为字符串,返回值为一个数字
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
void showtid(pthread_t &tid)
{
// //typedef unsigned long int pthread_t; -> %ld
// printf("新线程tid: %ld\n", tid);
printf("新线程tid: 0x%lx\n", tid); // 十六进制显示线程id
}
std::string FormatId(pthread_t &tid)
{
char id[64];
// snprintf(id, sizeof(id), "%ld", tid);
snprintf(id, sizeof(id), "0x%lx", tid);
return id;
}
void *routine(void *args)
{
std::string name = static_cast<const char *>(args);
int cnt = 3;
// 证明pthread_create输出的id就是新线程的id->pthread_self
pthread_t tid = pthread_self();
while (cnt--)
{
// std::cout << "我是新线程, 我的名字是: " << name << std::endl;
std::cout << "我是新线程, 我的名字是: " << name << ", 我的ID是: " << FormatId(tid) << std::endl;
sleep(1);
}
// return nullptr;
return (void *)100; //暂时理解:线程退出时的退出码
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void *)"thread--1");
// pthread_create获取到线程id,进行打印
showtid(tid);
int cnt = 3;
// 新线程有自己的id,main自然也有自己的线程id
pthread_t mtid = pthread_self();
while (cnt--)
{
std::cout << "我是main线程, 我的ID是: " << FormatId(mtid) << std::endl;
sleep(1);
}
// 线程等待pthread_join
//线程的返回值
void *ret = nullptr; //ret也是一个变量,也有空间!
// pthread_join(tid, nullptr);
pthread_join(tid, &ret);
std::cout << "ret: " << (long long int)ret << std::endl; //由于ret返回类型是void*指针,所以强转类型方便观察
return 0;
}

2.6.2 深度逻辑:为什么参数是二级指针 void **?
第一次见到 pthread_join 的第二个参数时,有些人会不理解为什么传参类型是二级指针。
本质需求:子线程返回的是一个地址(指针),主线程想用自己本地的一个变量(也是指针)去接收它。
传参原理:在 C 语言中,如果你想在函数内部修改外部变量的值,必须传递该变量的地址。
推导结论:
-
修改 int,传 int*。
-
修改 void*(指针),自然要传 void**(指针的地址)。

2.6.3 构建任务:可以传递C++中的类对象
在上面的代码演示中我们传入的参数是字符串,而返回类型就是简单的整型,那传入的参数和返回值可以是其他类型吗?通过参数类型是 void * 我们就知道:当我们给线程传递参数和返回值时,可以是任意类型。
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
// 1、main函数结束,代表主线程结束,一般而言也代表进程结束
// 2、新线程对应的入口函数,运行结束,代表当前线程运行结束
// 3、当我们给线程传递参数和返回值时,可以是任意类型
// 传递C++中的类对象
class Task
{
public:
Task(int a, int b) : _a(a), _b(b) {}
int Excute() { return _a + _b; }
~Task() {}
private:
int _a;
int _b;
};
class Result
{
public:
Result(int result) : _result(result) {}
int GetResult() { return _result; }
~Result() {}
private:
int _result;
};
void *routine(void *args)
{
Task *t = static_cast<Task *>(args); // args类型为void *,需强转回Task *
sleep(1);
Result *ret = new Result(t->Excute());
sleep(1);
return ret;
// return (void *)10;
}
int main()
{
pthread_t tid;
Task *t = new Task(10, 20); // 在堆上申请空间创建一个对象
// pthread_create(&tid, nullptr, routine, (void *)"thread--1");
pthread_create(&tid, nullptr, routine, t);
// void *ret = nullptr;
// pthread_join(tid, &ret);
// std::cout << "新线程结束, 退出码: " << (long long int)ret << std::endl;
Result *ret = nullptr;
pthread_join(tid, (void **)&ret);
std::cout << "新线程结束, 运行结果: " << ret->GetResult() << std::endl;
delete t;
delete ret;
return 0;
}

所以,给线程可以传递的参数可以很多!无非就是增加类内的成员属性------格局打开。
2.7 线程终止:pthread_exit && pthread_cancle
如果需要只终止某个线程而不终止整个进程,可以有三种方法:

那就有人问了,之前我们学习进程的时候不是也讲了一个终止的函数 exit() 吗,能不能用这个函数来终止线程?我们试一下。

我们会发现什么打印结果都没有了,因为exit()是用来终止进程的,当线程结束前调用这个函数,则整个进程直接结束,而主线程还在等待新线程退出,也就没有任何打印结果了。

2.7.1 pthread_exit函数:线程主动终止自身
线程内调用该函数,主动终止自身,效果和 return 一致,返回值可被pthread_join获取。
函数原型:
cpp
void pthread_exit(void *value_ptr);
- 参数value_ptr :线程退出的返回值,不能指向线程栈内的局部变量;
- **返回值:**无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

需要注意,pthread_exit 或者 return 返回的指针所指向的内存单元 必须是全局的或者是用malloc分配 的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
2.7.2 pthread_cancel函数:终止同进程的其他线程
一个线程调用该函数,取消同一个进程内的另一个线程,被取消的线程默认退出码是PTHREAD_CANCELED(值为 - 1)。
函数原型:
cpp
int pthread_cancel(pthread_t thread);
- 参数thread:要终止的目标线程 ID;
- 返回值:成功返回 0,失败返回错误码。
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
class Task
{
public:
Task(int a, int b) : _a(a), _b(b) {}
int Excute() { return _a + _b; }
~Task() {}
private:
int _a;
int _b;
};
class Result
{
public:
Result(int result) : _result(result) {}
int GetResult() { return _result; }
~Result() {}
private:
int _result;
};
// 线程终止的问题:
// 1、线程的入口函数,进行return就是线程终止
// 注意:线程一定不能用exit()终止,因为exit()是用来终止进程的!
// 2、pthread_exit()
// 3、pthread_cancle():线程被取消,退出结果为-1【PTHREAD_CANCELED】
void *routine(void *args)
{
Task *t = static_cast<Task *>(args); // args类型为void *,需强转回Task *
// 让新线程长时间等待主线程取消新线程
sleep(100);
Result *ret = new Result(t->Excute());
sleep(1);
// return ret;
// exit(13);
pthread_exit(ret);
std::cout << "新线程已退出, 无法看到当前信息" << std::endl;
}
int main()
{
pthread_t tid;
Task *t = new Task(10, 20); // 在堆上申请空间创建一个对象
pthread_create(&tid, nullptr, routine, t);
// 在主线程取消新线程
sleep(3);
pthread_cancel(tid); // 新线程不是正常结束退出
std::cout << "新线程已经被取消" << std::endl;
void *ret = nullptr;
pthread_join(tid, &ret);
std::cout << "新线程结束, 运行结果: " << (long long)ret << std::endl;
// #define PTHREAD_CANCELED ((void *) -1)
return 0;
}

2.8 线程分离:pthread_detach
通过上面的代码示例我们会发现一个问题就是:如果我们创建了新线程,则需要等待新线程退出,调用的函数就是 pthread_jion,而且我们会发现这个函数只能传入一个参数,也就是说它并不能像之前学习进程时讲进程等待 waitpid 存在第二个参数用来控制是阻塞还是非阻塞等待子进程 ,如果想要非阻塞等待子进程退出则传入SIGCHLD 即可,而线程就没有非阻塞等待一说。
那我们能不能做到对新线程不关心呢?是可以的。
我们把线程创建出来,默认 该线程必须被 join 的(joinable )!如果不想等,也不关心线程的退出情况------可以把目标线程设置为分离状态!(一旦线程被设置为分离状态,就不需要关心线程的退出情况,不需要等待了,主线程是最后退出的)
函数原型:
cpp
int pthread_detach(pthread_t thread);
- 参数thread:要分离的目标线程 ID;
- 返回值:成功返回 0,失败返回错误码。
使用方式:
- 其他线程分离:主线程创建子线程后,调用 **pthread_detach(tid)**分离子线程;
- 线程自分离:子线程内调用 **pthread_detach(pthread_self())**分离自身。
完整实战代码:线程分离
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <cstring>
void *routine(void *args)
{
//新线程把自己分离出去(pthread_self获取自己的线程ID)
pthread_detach(pthread_self());
int cnt = 5;
while(cnt--)
{
std::cout << "我是新线程" << std::endl;
sleep(1);
}
return nullptr;
}
//如果主线程不想关心新线程,而是当新线程结束的时候,让它自己去释放?
//设置新线程为分离状态
//技术层面:线程默认是需要被等待的(joinable),如果不想让主线程等待新线程
//想让新线程结束之后,自己退出,设置为分离状态(!joinable or detach) //T0D0
//理解层面:线程分离,可能是主线程主动分离新线程,也可能是新线程把自己分离出去
//(生活例子:家庭可能存在爹不称职儿子不认这个爹,也可能存在儿子坏事做尽爹不认这个儿子)
//但是分离的线程,依旧在同一个进程的地址空间中,进程的所有资源,被分离的线程依旧可以访问,可以操作
//处于分离状态的线程(不管是被其他线程分离还是自己分离)不需要进行join,join会失败!
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void *)"thread--1");
// //主线程分离新线程
// pthread_detach(tid);
// std::cout << "新线程被分离了" << std::endl;
int cnt = 5;
while(cnt--)
{
std::cout << "我是主线程" << std::endl;
sleep(1);
}
//被分离的线程不能被join!
void *ret = nullptr;
int n = pthread_join(tid, &ret);
if(n != 0)
{
std::cout << "pthread_join error: " << strerror(n) << std::endl;
}
else
{
std::cout << "pthread_join success: " << strerror(n) << std::endl;
}
return 0;
}


2.9 创建多线程
前面我们花了较大篇幅只是讲解了关于线程的相关接口的使用,使用的代码示例也只是创建了一个线程来讲解,但还没有对一个进程连续创建多个线程,通过学习了解了线程的相关接口我们尝试对一个进程连续创建多个线程。
但我们的代码也只是一个示例,目的是引出一个问题:
创建线程函数 pthread_create 的第四个参数 void *arg,究竟是选择在栈上开辟空间还是选择在堆上开辟空间?
这个问题是什么意思等会在写代码展示的结果就知道了。
2.9.1 传参在栈上开辟空间(错误做法)
我们先选择传参在栈上开辟空间,看一下结果是什么样的:
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <vector>
// 创建多线程代码示例
// 着重讲解传参在栈上和在堆上开辟空间的区别
const int nums = 5;
const int gsize = 64;
void *routine(void *args)
{
sleep(1);
std::string name = static_cast<const char *>(args);
std::cout << "我是线程" << name << ", 运行1s后退出" << std::endl;
sleep(1);
return nullptr;
}
int main()
{
std::vector<pthread_t> tids; // 将创建的多个线程id维护起来
for (int i = 0; i < nums; i++)
{
pthread_t tid;
// bug??
char threadname[gsize]; // 在栈上开辟空间
snprintf(threadname, sizeof(threadname), "thread-%d", i + 1);
std::cout << "创建新线程" << threadname << std::endl;
int n = pthread_create(&tid, nullptr, routine, threadname);
if (n == 0)
{
tids.push_back(tid);
}
else
continue;
}
// 等待全部线程退出
for (int i = 0; i < nums; i++)
{
//一个一个的等待
int n = pthread_join(tids[i], nullptr);
if (n == 0)
{
std::cout << "等待线程退出成功" << std::endl;
}
}
return 0;
}

我们会发现打印的结果其中的确出现了问题,那为什么会导致这种情况呢?直接给出解释:

而由于for循环执行的非常快,就导致了前面创建的线程还没来得及使用之前的值,就被后来修改的值进行了覆盖(本质原因就是因为地址相同),但这是栈本身的特性而导致的。
那这里就会有人提出问题了:在文章的最开始我们不是讲了对于线程而言哪些是共享资源哪些是私有资源,并且着重强调了对于线程而言有两个东西是线程私有的:寄存器上下文 ;独立栈空间 。
那不对啊,不是刚讲了线程有自己独立的栈空间吗?为什么这里在主线程自己内部创建的局部变量会被其他线程"共享"呢?主线程不也有自己独立的栈空间吗?这不产生了矛盾?
其实并没有,原因如下:

2.9.2 传参在堆上开辟空间(正确做法)
为了解决上面的问题,本质就是需要解决怎么让地址不同,所以正确的做法是传参在堆上开辟空间:
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <vector>
// 创建多线程代码示例
// 着重讲解传参在栈上和在堆上开辟空间的区别
const int nums = 5;
const int gsize = 64;
void *routine(void *args)
{
sleep(1);
std::string name = static_cast<const char *>(args);
std::cout << "我是线程" << name << ", 运行1s后退出" << std::endl;
sleep(1);
return nullptr;
}
int main()
{
std::vector<pthread_t> tids; // 将创建的多个线程id维护起来
for (int i = 0; i < nums; i++)
{
pthread_t tid;
// bug??
// char threadname[gsize]; // 在栈上开辟空间
// 正确做法:传参在堆上开辟空间
char *threadname = new char[gsize];
// snprintf(threadname, sizeof(threadname), "thread-%d", i + 1);
snprintf(threadname, gsize, "thread-%d", i + 1);
std::cout << "创建新线程" << threadname << std::endl;
int n = pthread_create(&tid, nullptr, routine, threadname);
if (n == 0)
{
tids.push_back(tid);
}
else
continue;
}
// 等待全部线程退出
for (int i = 0; i < nums; i++)
{
// 一个一个的等待
int n = pthread_join(tids[i], nullptr);
if (n == 0)
{
std::cout << "等待线程退出成功" << std::endl;
}
}
return 0;
}

结束语
本篇我们完整梳理了进程与线程的资源划分,以及线程创建、终止、等待、分离的全生命周期控制,这是 Linux 多线程开发的核心基础。理解了资源共享与私有,就能避开多线程开发的大部分坑;掌握了线程控制接口,就能完成基础的多线程程序开发。后续我们会继续深入线程安全、同步互斥、互斥锁、条件变量等进阶内容。