040 线程控制

040 线程控制
小米里的大麦线程控制
1. POSIX 线程库
1. 什么是 POSIX 线程库(pthread)
POSIX(Portable Operating System Interface)线程库,又称(简称) pthread(POSIX Threads),是 Unix 系统下的标准化多线程编程接口(IEEE POSIX 标准(IEEE 1003.1c)定义的线程接口)。它提供了一组函数,用于在同一进程内创建、管理和同步多个线程,实现并发和并行处理。
pthread 线程库是应用层的原生线程库: 应用层指的是这个线程库并不是系统接口直接提供的,而是由第三方帮我们提供的。大部分 Linux 系统都会默认带上该线程库(原生的)。与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以 pthread_
打头的。要使用这些函数库,要通过引入头文件 <pthreaad.h>
,链接这些线程函数库时,要使用编译器命令的 -lpthread
选项。
2. 特点
- 与操作系统紧密集成,性能开销小。
- 接口统一,可移植性好。
- 支持线程同步(互斥锁、条件变量)、线程属性设置等丰富功能。
- 线程共享同一进程的内存空间(代码段、堆、全局变量等)。
- 线程间通信更高效(直接访问共享数据)。
- 适用于需要高并发的场景(如服务器、实时处理)。
3. pthread_t
1. 功能
pthread_t
是 pthread 库定义的线程标识类型,类似于进程中的 PID。每个创建的线程都会分配一个唯一的 pthread_t
值,用来引用和管理该线程。
2. 本质
Linux 下,pthread_t
通常以整数或指针的形式存在(与 glibc 实现有关),我们只需将其当作“黑盒”标识符,配合其他 pthread 函数即可。
1 |
|
4. pthread_create —— 创建新线程
1. 功能
创建一个新线程,并让它去执行用户定义的函数。
2. 函数原型
1 |
|
3. 参数详解
thread
:指向pthread_t
的指针,函数返回后通过它获取新线程的 ID;attr
:线程属性指针,可设置线程栈大小、分离状态等,通常传NULL
/nullptr
;start_routine
:线程函数指针,必须形如void* func(void*)
;arg
:传给线程函数的单个参数,可是任意指针(如结构体、基本类型地址),需要在函数内强转回原类型。
4. 返回值
POSIX 线程(pthreads)函数在出错时 不设置全局 errno ,而是 直接通过返回值返回错误码 (成功为 0,失败为非 0 值)。这与传统系统调用(如 open、read)不同,传统调用通常返回-1 并设置 errno。pthreads 这样做是为了避免多线程环境下对全局 errno 的竞争,提升性能和可移植性。尽管每个线程有独立的 errno 以兼容其他使用它的代码,但 建议始终检查 pthreads 函数的返回值来判断错误 ,而不是依赖 errno。
- 返回
0
:创建成功; - 返回非
0
:错误码,表示创建失败(如资源不足、权限问题等)。
5. 代码示例
1 |
|
编译(-lpthread):
编译命令
1
g++ -o pthread_create pthread_create.cc -lpthread
-lpthread
:链接 pthread 库,必须加在源文件或对象文件之后。makefile 文件:
1
2
3
4
5pthread_create:pthread_create.cc
g++ -o $@ $^ -std=c++11 -lpthread
clean:
rm -f pthread_create
运行结果示例:
原子性 指一个操作要么 完全执行成功 ,要么 完全没执行 ,在执行过程中 不会被中断或分割 。原子性 = 不可分割性,一个操作如果是原子的,就 不会被其他线程或中断打断 ,外界看起来就像“瞬间完成”。
6. 线程监控与查看
查看进程和线程
ps -AL
:列出所有线程(Lightweight Process,LWP)1
ps -AL | grep pthread_create # 输出中,LWP 列就是线程 ID
查看可执行文件依赖
ldd
:列出可执行文件所依赖的共享库1
ldd pthread_create # 可以确认是否已正确链接 libpthread.so
结合 top 或 htop
- 在
top
中,按H
可切换到线程视图; - 便于实时监控各线程的 CPU/内存占用情况。
- 在
以下是命令的逐步解析:
1.
ps axj | head -1 & ps axj | grep pthread_create | grep -v grep
ps axj
:ps
是 Linux 系统中用于查看进程状态的命令。-a
:显示所有终端上的进程,包括其他用户的进程。-x
:显示没有控制终端的进程。-j
:以长格式显示线程信息,包括线程 ID、进程组 ID、会话 ID 等。head -1
:只取第一行,通常是表头信息。grep pthread_create
:过滤出包含字符串pthread_create
的行,这些行通常与使用了pthread_create
函数创建的线程相关。grep -v grep
:排除包含grep
自身的行,避免干扰。2.
ps -aL | head -1 & ps -aL | grep pthread_create | grep -v grep
ps -aL
:-a
:显示所有终端上的进程。-L
:显示线程信息,类似于-j
,但格式稍有不同。3.
ldd pthread_create
ldd
:显示指定可执行文件或共享库所依赖的动态链接库。
clone()
是 Linux 提供的底层系统调用,用于创建子进程或线程,是fork()
和pthread_create()
的核心实现基础之一。相比fork()
,它更灵活,可以通过传入不同的标志位来控制父子进程(或线程)之间是否共享地址空间、文件描述符、信号处理等资源,从而实现“线程”效果。因为使用较复杂(需要手动分配栈空间等),只做了解,不推荐直接使用。头文件:<sched.h>
,函数原型:
1 >int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...);
2. 线程的调度
1. 线程调度单位到底是谁?
“应用层的线程与内核的 LWP 是一一对应的,调度的是 LWP 而非 PID。”在 Linux 中:调度单位是 LWP(轻量级进程),而不是进程本身(PID)。
可以理解为:
概念 | 含义 |
---|---|
应用层线程 | 即 pthread_create 创建的线程 |
内核 LWP | 每个线程在内核中的调度实体 |
PID 与 LWP | 主线程的 LWP ID 与 PID 相等,子线程的 LWP ID 不等于 PID |
调度单位 | Linux 中调度的是每个线程(LWP),不是进程整体 |
2. 什么是 LWP(Light Weight Process)?
LWP 是 Linux 内核中的 最小调度单位,本质上就是一个“执行上下文”:包括程序计数器、栈、寄存器、调度信息等。每个 LWP 都有自己的 ID,即在 ps -AL
中看到的 LWP(Thread ID)。它们共享 所属进程的虚拟内存空间、打开的文件、信号处理器等资源。
3. Linux 中 pthread 与 LWP 的关系?
在 Linux 上,每个 pthread 线程 = 一个 LWP。
pthread_create()
创建的每一个线程,都会在内核中映射成一个 LWP;- 所以 Linux 是通过调度多个 LWP 来实现多线程程序并发运行(先描述再组织)。
4. 为什么说 “我们以前接触到的都是单线程进程,PID 和 LWP 相等”?
单线程程序只有一个主线程,所以它只有一个 LWP。而这个 LWP 的 ID(TID)刚好等于进程 ID(PID),getpid() == gettid()
(在主线程中成立)。但如果在程序中创建了多个线程(用 pthread),就会发现:
getpid()
(获取进程 ID)在每个线程中都一样;gettid()
(获取线程 ID)每个线程都不同;ps -AL
或top -H
会列出多个线程,每个线程一个 TID(内核调度单位);
系统调度的是 LWP(线程),不是进程(PID),这就是为什么:
- 多线程程序中,真正被调度运行的是每个线程(LWP)。
- 哪些线程先运行,哪些后运行,完全由调度器决定(不是你代码里的顺序)。
- 每个 LWP 都可能在不同的 CPU 核心上并发运行(多核 CPU)。
5. pthread_self —— 获取线程 ID
1. 功能
获取 当前线程自身的线程 ID(pthread 库中的 ID 类型 pthread_t
),用于线程内部识别自身,或与其他线程 ID 进行比较。
2. 函数原型
1 |
|
- 无参数,不需要传入任何值,它自动返回当前线程对应的
pthread_t
。 - 返回值类型是
pthread_t
,表示当前线程的 ID。
3. 返回值
返回 当前线程的 ID(类型为 pthread_t
),可以用这个 ID:
- 打印出来查看当前线程是谁;
- 与其他
pthread_t
对比,判断是不是同一个线程;注意:pthread_t
是个不透明类型,比较是否相等,应使用pthread_equal()
函数。 - 在调试或日志记录中标识线程身份。
4. 代码示例
1 |
|
运行结果示例:
1 | [hcc@hcss-ecs-be68 Threads]$ ./pthread_self |
主线程和子线程的 pthread_self()
返回值不同,说明它们是两个独立的线程。两个 tid
变量虽然名字类似,但位于不同线程的栈上。多线程环境下,函数的局部变量是线程安全的(自动隔离)。
3. 线程等待
1. 线程等待是什么?
在多线程程序中,主线程或其他线程可能需要等待某个线程执行完毕后再继续执行。这个等待的过程叫做 线程等待 。类似于进程中的 wait()
系统调用。
2. pthread_join —— 阻塞线程
1. 功能
阻塞当前线程,直到指定的线程结束,并可 获取该线程的返回值(退出码)。
- 常用于 主线程等待子线程完成任务;
- 可以在子线程中
return
或使用pthread_exit()
返回一个结果; - 主线程通过
pthread_join()
把这个返回值拿到。
2. 函数原型
1 |
|
3. 参数详解
参数名 | 类型 | 说明 |
---|---|---|
thread |
pthread_t |
要等待的线程 ID,一般是 pthread_create 时返回的 |
retval |
void** |
二级指针,接收线程退出时的退出码信息(可以为 NULL ) |
retval 的注意:
- 若不关心线程返回什么,可以传
NULL
/nullptr
; - 若关心,则要定义
void* result
,传&result
,线程退出时返回值会保存在result
中。
4. 返回值
- 成功:返回 0。
- 失败:返回非 0,即错误码(如无效的线程 ID、线程不存在等)。
5. 代码示例
1 |
|
运行结果示例:
1 | [hcc@hcss-ecs-be68 pthread_join]$ ./pthread_join |
3. 线程退出值 = 退出码(为啥只能拿这个)?
pthread_join()
是主线程 阻塞等待 子线程完成,并 获取子线程返回值(退出码) 的唯一方式,而这个退出码只能是void*
类型,因为 POSIX pthread 模型就规定线程函数只能返回一个void*
指针。
1. 为什么线程退出时只能“返回一个退出码”?
因为线程函数的原型是:
1 | void* (*start_routine)(void*) |
- 它只能有一个返回值,类型是
void*
,这是 POSIX 标准设计决定的; - 无法直接返回多个值,也不能返回栈上对象(因为线程函数退出后栈就销毁了);
- 所以如果需要返回复杂数据,必须 动态申请内存(如
new
)并 return 指针,主线程通过pthread_join()
接收并自行释放。
2. 为什么不是像 fork()
那样返回整型或 exit code?
fork()
是 进程级别,操作系统可以记录 exit status;pthread_exit()
或return
是线程级别,线程之间共享地址空间,退出值不需要写入操作系统状态;pthread_join()
只关心线程退出时返回的那块 用户级别的数据指针(void*),而不是操作系统的退出码。
4. 线程终止
1. 线程终止的 4 种方式(核心)
在 POSIX pthread
中,线程的终止方式主要有以下几种:
线程函数运行完毕后自动返回: 这是最自然的退出方式,函数体执行到最后,线程就自动退出。
在函数中调用
pthread_exit()
主动退出: 这种方式适用于希望 中途退出线程,但又希望返回一个退出值的情况。可随时退出线程、能设置退出码、等效于return
。其他线程调用
pthread_cancel()
强制取消线程: 一种 异步控制 方式。线程不一定立即退出,需要处于“可取消点”(如 sleep、read 等),线程退出码为PTHREAD_CANCELED ((void*)-1)
。注意:如果线程没有设置为可取消状态,pthread_cancel()
无效。整个进程退出时,所有线程终止: 当 主线程调用
exit()
、_exit()
或主线程崩溃 导致整个进程终止时,所有线程也会强制结束。粗暴退出会导致线程无法清理资源,常见于崩溃或异常退出。
终止方式 | 触发者 | 是否能传退出码 | 是否立即退出 | 是否安全 |
---|---|---|---|---|
return |
本线程自己 | ✅ 是 | ✅ 是 | ✅ 推荐 |
pthread_exit() |
本线程自己 | ✅ 是 | ✅ 是 | ✅ 推荐 |
pthread_cancel() |
其他线程 | ✅(默认为 PTHREAD_CANCELED ) |
❌ 依赖可取消点 | ⚠️ 谨慎使用 |
exit() / 崩溃 |
任意线程 | ❌ 无法获取 | ✅ 是 | ❌ 不推荐 |
2. pthread_exit
—— 主动退出当前线程
1. 功能
主动退出当前线程,并返回一个退出值(给等待该线程的其他线程)。比 return
更灵活,可在任何地方终止线程。
适用于:
- 线程需要在函数中间提前退出;
- 想设置返回值供
pthread_join
获取;
2. 函数原型
1 |
|
3. 参数详解
void* retval
: 线程的退出码,可传任意指针或整型强转。是 返回给 pthread_join 的退出值,如果线程已分离,该值会被忽略。
4. 返回值
无返回值,调用后线程立即退出,后面的代码不会执行。
5. 代码示例
1 |
|
运行结果示例:
1 | [hcc@hcss-ecs-be68 pthread_exit]$ ./pthread_exit |
3. pthread_cancel
—— 发送请求
1. 功能
向指定线程发送“取消请求”,尝试强制终止它(但不一定立即终止)。
适用于:
- 主动终止长时间运行或卡死的线程;
- 线程之间的控制与协作场景。
2. 函数原型
1 |
|
3. 参数详解
pthread_t thread
: 目标线程的线程 ID。
4. 返回值
返回值 | 含义 |
---|---|
0 |
成功发送取消请求(注意:不是线程已退出!) |
ESRCH |
没有找到指定的线程 ID(线程不存在) |
EINVAL (少见) |
线程 ID 无效(某些实现中使用) |
5. 注意事项
- 不是立即强制终止线程!
- 被取消线程 必须处于可取消状态,且处于 取消点;
- 常见取消点有
sleep
、read
、pthread_join
等。
- 线程取消成功后,其退出值为
(void*)PTHREAD_CANCELED
,用来判断线程是否被取消。 - 如何查一个函数是不是取消点?
- 查阅 POSIX 官方文档
- 使用命令:
man 7 pthreads
,在 Linux 手册中输入/
搜索 “Cancellation points”,会列出所有标准取消点函数。
PTHREAD_CANCELED
是 POSIX 线程库(pthread)中一个 宏常量,它 用于判断/标识线程是否是被pthread_cancel()
强制取消退出的,而不是正常执行完毕返回的,可以在pthread_join()
后检查返回值是否等于它,来确认线程状态。其本质是个#define
(宏)
1 >
- 它是一个
void*
类型的宏常量;- 实际上是
(void*)-1
,特殊指针,用作标记;- 不能拿它当做真实的返回值内容使用,只能 判断它是不是等于某线程的退出码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 >
>
>
>using namespace std;
>void* func(void* arg)
>{
while (1)
{
cout << "子线程运行中..." << endl;
sleep(1);
}
>}
>int main()
>{
pthread_t tid;
pthread_create(&tid, nullptr, func, nullptr);
sleep(2); // 让子线程跑一会儿
pthread_cancel(tid); // 取消子线程
void* retval;
pthread_join(tid, &retval); // 等待子线程结束
// 检查返回值是否为被取消
if (retval == PTHREAD_CANCELED)
{
cout << "子线程被成功取消,返回值 = PTHREAD_CANCELED" << endl;
}
else
{
cout << "子线程正常退出,返回值 = " << retval << endl;
}
return 0;
>}
1
2
3
4
5 >[hcc@hcss-ecs-be68 pthread_cancel]$ g++ -o PTHREAD_CANCELED PTHREAD_CANCELED.cc -std=c++11 -lpthread
>[hcc@hcss-ecs-be68 pthread_cancel]$ ./PTHREAD_CANCELED
>子线程运行中...
>子线程运行中...
>子线程被成功取消,返回值 = PTHREAD_CANCELED
6. 代码示例
1 |
|
输出:
1 | [hcc@hcss-ecs-be68 pthread_cancel]$ ./pthread_cancel |
4. 小结
函数 | 主体是谁调用 | 用于哪个线程 | 是否立即终止 | 是否能设置退出码 |
---|---|---|---|---|
pthread_exit |
当前线程 | 自己 | 立即 | 是 |
pthread_cancel |
外部线程 | 目标线程 | 非立即 | 返回值为宏 |
线程分离(Detached Thread)是 Linux 多线程(POSIX 线程 pthread
)编程中的一个重要概念。下面我们从概念入手,逐步深入 pthread_detach
函数及其背后机制,讲清楚线程分离的本质。
5. 线程分离
1. 线程分离是什么,有什么用?
在默认情况下(joinable 模式),线程执行完后不会立即释放资源,需要其他线程调用 pthread_join()
与之回收,才能释放其占用的资源(如线程栈、PCB 结构等)。
线程分离(detached) 就是让线程在执行完毕后自动释放自己的资源,不再需要其他线程去 pthread_join()
它,防止资源泄漏。注意:线程一旦结束,系统自动回收资源,不能再被 pthread_join
!
2. pthread_detach
函数
1. 功能
将一个线程设置为 分离状态,使其结束时资源自动释放。
2. 函数原型
1 |
|
3. 参数说明
thread
:需要设置为分离状态的线程 ID(pthread_t
类型)。
4. 返回值
0
:成功。EINVAL
:线程不是 joinable 或状态无效。ESRCH
:指定的线程 ID 不存在。
5. 代码示例
1 |
|
运行结果示例:
1 | [hcc@hcss-ecs-be68 pthread_detach]$ ./pthread_detach |
3. 线程分离的本质是什么?
线程分离的本质就是线程的一个属性,叫作:
- PTHREAD_CREATE_JOINABLE(默认)
- PTHREAD_CREATE_DETACHED(分离)
这个属性决定了线程的生命周期如何管理资源:
- JOINABLE 状态:线程执行完还要别人
join()
回收资源; - DETACHED 状态:线程执行完直接自己清理干净,别人不能
join()
它。
一旦线程设置为分离状态,无法再对它调用 pthread_join()
,否则会导致未定义行为,分离线程是否继续执行不取决于主线程是否退出,而取决于进程是否还活着。(分离线程 = 自动回收,非分离线程 = 手动回收)。
4. 小结
- 如果创建的线程是 短生命周期、且不需要结果回传的(如后台异步日志写入),建议使用
pthread_detach()
或属性设置为分离。 - 如果需要线程返回值(如子线程计算结果后回传主线程),则必须使用
joinable
并调用pthread_join()
。 - 线程分离后,即使主线程退出,分离线程也不一定会结束,只有当进程退出才会影响!
- 千万不要创建完线程后忘记
join()
或detach()
,否则会发生资源泄漏,尤其在线程频繁创建时。
6. C++ 语言层面上的多线程支持(C++11 起,了解)
pthread
是 C 语言的 POSIX 线程库(第三方),可在 C++ 中直接使用,但并非 C++ 语言原生支持;从 C++11 起,C++ 在语言层面提供了 std::thread
作为标准多线程支持,具有更好的类型安全和跨平台性,底层在 Linux 上通常基于 pthread
实现,但对用户透明。
1. pthread VS std:: thread
概念 | 说明 |
---|---|
pthread |
是 POSIX 标准定义的 C 语言线程 API,不是 C++ 的一部分。在 Linux 上通过 <pthread.h> 提供。 |
std::thread |
是 C++11 标准引入的 C++ 原生线程类,属于 C++ 标准库,头文件 <thread> 。 |
对比 | pthread (C 风格) |
std::thread (C++11 起标准库) |
---|---|---|
来源 | POSIX API(非 C++ 标准) | C++ 标准库(C++11 起) |
头文件 | <pthread.h> |
<thread> |
依赖 | POSIX 系统(Linux/Unix),Windows 原生不支持 | 跨平台:Windows / Linux / macOS(编译器支持即可) |
语言风格 | C 风格:函数指针 + void* 参数 |
C++ 风格:支持 lambda、成员函数、函数对象、模板 |
类型安全 | void* 传参,易出错 |
模板自动推导,类型安全 |
封装性 | 纯函数式调用,无类封装 | std::thread 是类,支持 RAII、移动语义 |
适合谁 | 系统编程、嵌入式、高性能定制、底层原理 | 应用开发、跨平台项目、现代 C++ 开发 |
退出机制 | pthread_exit() 、pthread_join() |
t.join() 、t.detach() ,析构时自动检查 |
可移植性 | 仅限 POSIX 系统 | 只要编译器支持 C++11 就可移植 |
底层实现 | 直接调用内核 LWP(轻量级进程) | 在 Linux 上通常基于 pthread 封装(但对用户透明) |
编译选项 | g++ -o app app.cpp -lpthread |
g++ -o app app.cpp -std=c++11 (自动链接) |
2. std::thread
vs pthread
对照表
pthread 函数 |
std::thread 写法 |
说明 |
---|---|---|
pthread_create(&tid, nullptr, func, arg) |
std::thread t(func, arg); |
创建线程 |
pthread_join(tid, &ret) |
t.join(); |
等待线程结束 |
pthread_exit() |
return; |
线程函数返回即退出 |
pthread_self() |
std::this_thread::get_id() |
获取当前线程 ID |
sleep(1) |
std::this_thread::sleep_for(1s); |
睡眠 1 秒 |
3. 代码示例
1. 创建线程
1 | thread t(函数名, 参数...); |
参数会自动拷贝(如果是对象)想传引用?用 std::ref(变量)
包一层。
1 | void func(int& x) { x = 100; } |
2. 等待线程结束:join()
1 | t.join(); // 必须调用,否则程序会崩溃! |
类比 pthread_join
,一个 thread
对象只能 join()
一次。
3. 分离线程:detach()
1 | t.detach(); // 不等它,让它后台运行 |
4. 获取线程 ID
1 | cout << "当前线程ID: " << this_thread::get_id() << endl; |
5. 支持 lambda(超方便!)
1 | thread t([]{ |
6. 支持类成员函数
1 | class Worker |
1 |
|
1 |
|
7. 可重入与线程安全
“可重入”指的是一个函数可以被多个线程同时调用,并且不会互相影响,不会出现混乱或崩溃。
1. 代码示例:一个不可重入的函数
1 |
|
这个函数是 不是可重入的,并且运行输出是错乱的,因为 counter
是 全局变量,多个线程同时改它,结果错乱。cout
也是 共享资源,多个线程同时输出可能出现换行错乱。
2. 可重入函数应该是什么样?
1 | // 完全不使用全局变量,只用局部变量 |
这个 safe_task()
就是 可重入的函数,每个线程都自己玩自己的变量,互不干扰。
3. 小结
现在用线程,只需要记住:可重入函数不使用全局变量,也不操作共享资源,就不会线程混乱。目前只需要做到:尽量只用局部变量,一个线程干自己的事,不要访问别人家的变量,就能避免 90% 的线程问题!
要注意的点 | 是否说明可重入 | 建议做法 |
---|---|---|
用全局变量 | ❌ 否 | 每个线程用自己的局部变量 |
打印输出 cout/printf | ❌ 否 | 少用或后续使用加锁保护输出 |
多个线程同时调函数 | ✅ 是 | 放心大胆用 |
pthread API | ✅ 无影响 | 这些 pthread 函数本身是线程安全的 |
8. 代码实战
1. 多种终止方式
1 |
|
2. 多线程的协同
1 |
|
3. 多线程特性综合演示
1 |
|