「Linux 系统编程」线程的概念与控制
1 线程的概念与特性
1.1 线程的概念
线程(Thread)是进程中的一个独立执行流,是操作系统能够进行运算调度的最小单位。一个进程可以包含多个线程,所有这些线程 共享进程的资源(如内存空间、文件描述符等),但每个线程拥有自己独立的栈空间和程序计数器。
可以将一个进程理解为一个工厂,而这个工厂里的多条流水线就是多个线程:
- 工厂(进程):拥有整体的资源(厂房、电力、原材料仓库)。
- 流水线(线程):工厂流水线上的工人,共享工厂的资源,但各个流水线并行地完成一些任务。
1.2 线程的特性
-
共享资源:同一进程下的所有线程共享大部分进程资源,如 代码段、数据段、堆空间、打开的文件 等。这使得线程间通信非常高效,直接读写共享内存即可,但也带来了同步问题。
-
私有资源:每个线程拥有自己 私有 的 栈空间(用于存储局部变量、函数调用链)、程序计数器(PC,指向下一条要执行的指令)和寄存器状态。
-
轻型实体:创建、终止、切换线程的开销远小于进程,因为操作系统不需要为新的执行流分配全新的内存空间和资源表。但是,如果创建的线程数量非常非常多,可能会造成反效果。
-
并行执行:在多核处理器上,多个线程可以真正并行运行,从而大幅提高程序的吞吐量和响应速度。
2 Linux C 多线程核心函数
Linux 下的多线程编程主要遵循 POSIX 线程标准(pthread)。编译时需在 gcc 后加上 -pthread
选项。也可以 -lpthread
,但还是推荐 -pthread
。
1 |
|
特性 | -pthread |
-lpthread |
---|---|---|
标准性 | POSIX 标准推荐选项 | 传统链接器选项 |
功能范围 | 编译和链接阶段的完整支持 | 仅链接库 |
宏定义 | 自动定义 _REENTRANT 等宏 |
不自动定义任何宏 |
编译器优化 | 可能启用线程相关的编译器优化 | 无额外优化 |
可移植性 | 更高,跨平台一致性更好 | 较低 |
推荐程度 | 推荐使用 | 传统用法 |
2.1 pthread_create()
创建线程
1 |
|
参数:
thread
:输出参数,用于 返回新线程的 ID。attr
:线程属性,通常设为 NULL 表示使用默认属性。start_routine
:线程函数指针。该函数的形式必须是void* func_name(void* arg)
。arg
:传递给线程函数的参数。
返回值:成功返回 0
,失败返回错误码(非零)。
2.2 pthread_exit()
终止线程
pthread_exit()
会显式地终止线程。
1 |
|
参数:
retval
:线程的退出状态,可以被 pthread_join()
获取。注意:不能返回一个局部变量的指针。
2.3 pthread_join()
等待线程结束
pthread_join()
阻塞当前线程,直到指定 join 的目标线程结束。并且回收其资源。
pthread_join()
可以防止产生“僵尸线程”,确保线程资源被正确回收。
1 |
|
参数:
thread
:要等待的线程 ID。retval
:输出参数,用于获取目标线程的退出状态(即它 调用pthread_exit()
或 return 时传递的值)。如果不需要,可以设为NULL
。
返回值:成功返回 0
,失败返回错误码。
2.4 pthread_detach()
分离线程
将线程设置为“分离状态”(detached state)。目标线程被分离后,会脱离当前线程的控制,作为后台线程独立运行。
当前线程不再等待目标线程执行完毕。如果当前线程提早结束,目标线程仍可能继续运行(直到其自身逻辑结束)。除非进程终止时所有线程会被强制终止。
分离线程结束后,其占用的资源(如线程栈、控制块)会自动释放,无需当前线程 join 回收。并且 detach 过后的线程本来就不能再进行 join。
1 |
|
参数:
thread
:要分离的线程 ID。
返回值:成功返回 0,失败返回错误码。
分离线程的适用场景:"即发即忘"任务
- 日志记录:将日志写入操作放在分离线程中
- 定期清理任务:内存清理、缓存刷新等
- 异步通知:发送通知而不阻塞主线程
- 监控任务:定期检查系统状态
总结:pthread_detach
创建的线程可以理解为 在后台运行,但仍然 与进程同生共死,并且 无法再与主线程进行同步或通信。
3 Linux C 多线程核心函数使用示例
3.1 pthread_create
和 pthread_join
示例
创建了两个线程,执行线程函数 foo()
。主线程执行的是 main()
,通过 pthread_join
等待两个子线程执行完毕。两个子线程并发执行,会产生交替打印的效果。
1 |
|
3.2 pthread_detach
示例
创建一个分离线程,主线程不等待它。分离线程可以被看作在后台运行,不再受到主线程的控制。分离线程的资源会在主进程退出前被自动回收
1 |
|
4 pthread_cancel
函数及使用
4.1 pthread_cancel
函数
pthread_cancel
:向指定的线程发送一个取消请求(cancellation request)。它不会立即强制终止线程,而是礼貌地“请求”目标线程终止自己。目标线程是否会终止、何时终止,取决于其自身的取消性状态(cancellation state)和类型(cancellation type)。
1 |
|
参数:
thread
:要取消的目标线程的 ID。
返回值:
- 成功返回 0。
- 失败返回一个非零的错误码(注意:不是设置 errno)。
4.2 线程的取消机制(状态和类型)
线程对于取消请求的响应行为是由两个属性共同决定的:
-
取消状态 (Cancellation State)
PTHREAD_CANCEL_ENABLE
(默认值)- 线程可以接收取消请求。这是新创建线程的默认状态。
PTHREAD_CANCEL_DISABLE
- 线程忽略所有接收到的取消请求。取消请求会被挂起,直到线程的取消状态再次变为
ENABLE
。
- 线程忽略所有接收到的取消请求。取消请求会被挂起,直到线程的取消状态再次变为
-
取消类型 (Cancellation Type)
PTHREAD_CANCEL_DEFERRED
(默认值)- 延迟取消。线程不会立即终止,只有当它执行到一个取消点 (cancellation point) 时,取消请求才会被处理。这是新创建线程的默认类型。
PTHREAD_CANCEL_ASYNCHRONOUS
- 异步取消。线程可以在任何时间点被立即取消,取消点不再是必须的。不过非常危险,因为它可能发生在线程正在修改共享数据或持有锁的时候,极易导致状态不一致或死锁。
-
取消点 (Cancellation Point)
取消点是一些可能引起阻塞的库函数。当线程(处于
ENABLE
和DEFERRED
状态)执行这些函数时,会 检查是否有挂起的取消请求,如果有,就会开始处理取消操作。常见的取消点包括:
sleep()
,usleep()
,nanosleep()
read()
,write()
,wait()
,waitpid()
pthread_join()
,pthread_cond_wait()
,pthread_cond_timedwait()
printf()
,scanf()
(标准IO函数通常是)open()
,close()
- 几乎所有可能阻塞的系统调用。
可以使用
pthread_testcancel()
函数来手动创建一个取消点。如果一个执行长时间计算的线程没有自然到达任何取消点,你应该定期调用此函数来检查取消请求。
4.3 取消状态和类型相关函数
线程可以通过以下函数来管理自己的可取消性:
1 |
|
-
pthread_setcancelstate()
: 设置取消状态(ENABLE 或 DISABLE),并可选择保存旧状态。 -
pthread_setcanceltype()
: 设置取消类型(DEFERRED 或 ASYNCHRONOUS),并可选择保存旧类型。 -
pthread_testcancel()
: 如果当前线程有挂起的取消请求,则此函数会触发取消操作,否则直接返回。
4.4 线程清理处理程序
当一个线程被成功取消时,为了确保资源(如已分配的堆内存、已打开的锁等)能够被正确释放,必须使用清理处理程序。
1 |
|
pthread_cleanup_push()
: 将一个清理函数routine
压入当前线程的清理栈。当线程被取消或调用pthread_exit()
时,栈中的所有清理函数会以相反的顺序(后进先出)被调用。arg
是传递给清理函数的参数。pthread_cleanup_pop()
: 从清理栈中弹出一个清理函数。如果execute
参数非零,则执行该清理函数;如果为 0,则只是弹出而不执行。
注意:
push
和pop
必须成对出现,且在同一个代码块内(同一个函数内、同一对花括号{}
内)。- 线程正常通过
return
结束时,清理处理程序不会被调用。
4.5 pthread_cancel
示例
创建一个线程,允许它被取消,并设置清理函数来安全地释放资源。
1 |
|
5 本篇与 C++ 多线程的区别
本篇内容对应 C++ 多线程的内容可见:C++ 多线程」并发与多线程、「C++ 多线程」join(), detach(), joinable()、「C++ 多线程」std::thread 线程函数参数传递 等。
在 C++ 中,没有提供直接强制终止或取消另一个线程的函数。主要是出于安全性和可靠性的考虑:
-
资源泄漏风险:强制终止线程可能导致:
- 内存泄漏(分配的内存未释放)
- 文件描述符泄漏(打开的文件未关闭)
- 互斥锁未解锁(导致死锁)
- 其他系统资源未正确释放
-
数据一致性风险:线程可能在修改共享数据时被终止,导致数据结构处于不一致状态
-
可预测性问题:无法确定线程在何时何地被终止,使程序行为难以预测和调试
如果要在 C++ 使用线程终止/取消的函数,通常会采用结合 标志变量 或者 条件变量(condition_variable
)等来安全实现。