「Linux 系统编程」线程的概念与控制

1 线程的概念与特性

1.1 线程的概念

线程(Thread)是进程中的一个独立执行流,是操作系统能够进行运算调度的最小单位。一个进程可以包含多个线程,所有这些线程 共享进程的资源(如内存空间、文件描述符等),但每个线程拥有自己独立的栈空间和程序计数器。

可以将一个进程理解为一个工厂,而这个工厂里的多条流水线就是多个线程:

  • 工厂(进程):拥有整体的资源(厂房、电力、原材料仓库)。
  • 流水线(线程):工厂流水线上的工人,共享工厂的资源,但各个流水线并行地完成一些任务。

1.2 线程的特性

  • 共享资源:同一进程下的所有线程共享大部分进程资源,如 代码段、数据段、堆空间、打开的文件 等。这使得线程间通信非常高效直接读写共享内存即可,但也带来了同步问题

  • 私有资源:每个线程拥有自己 私有栈空间(用于存储局部变量、函数调用链)、程序计数器(PC,指向下一条要执行的指令)和寄存器状态。

  • 轻型实体:创建、终止、切换线程的开销远小于进程,因为操作系统不需要为新的执行流分配全新的内存空间和资源表。但是,如果创建的线程数量非常非常多,可能会造成反效果。

  • 并行执行在多核处理器上多个线程可以真正并行运行,从而大幅提高程序的吞吐量和响应速度。



2 Linux C 多线程核心函数

Linux 下的多线程编程主要遵循 POSIX 线程标准(pthread)。编译时需在 gcc 后加上 -pthread 选项。也可以 -lpthread,但还是推荐 -pthread

1
2
gcc my_program -o my_program.c -pthread  # 推荐
gcc my_program -o my_program.c -lpthread
特性 -pthread -lpthread
标准性 POSIX 标准推荐选项 传统链接器选项
功能范围 编译和链接阶段的完整支持 仅链接库
宏定义 自动定义 _REENTRANT 等宏 不自动定义任何宏
编译器优化 可能启用线程相关的编译器优化 无额外优化
可移植性 更高,跨平台一致性更好 较低
推荐程度 推荐使用 传统用法

2.1 pthread_create() 创建线程

1
2
3
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);

参数

  • thread:输出参数,用于 返回新线程的 ID
  • attr:线程属性,通常设为 NULL 表示使用默认属性。
  • start_routine:线程函数指针。该函数的形式必须是 void* func_name(void* arg)
  • arg传递给线程函数的参数

返回值:成功返回 0,失败返回错误码(非零)。


2.2 pthread_exit() 终止线程

pthread_exit() 会显式地终止线程。

1
2
#include <pthread.h>
void pthread_exit(void *retval);

参数

retval线程的退出状态,可以被 pthread_join() 获取。注意:不能返回一个局部变量的指针


2.3 pthread_join() 等待线程结束

pthread_join() 阻塞当前线程,直到指定 join 的目标线程结束。并且回收其资源

pthread_join() 可以防止产生“僵尸线程”,确保线程资源被正确回收。

1
2
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

参数

  • thread:要等待的线程 ID。
  • retval:输出参数,用于获取目标线程的退出状态(即它 调用 pthread_exit() 或 return 时传递的值)。如果不需要,可以设为 NULL

返回值:成功返回 0,失败返回错误码。


2.4 pthread_detach() 分离线程

将线程设置为“分离状态”(detached state)。目标线程被分离后,会脱离当前线程的控制,作为后台线程独立运行

当前线程不再等待目标线程执行完毕。如果当前线程提早结束,目标线程仍可能继续运行(直到其自身逻辑结束)。除非进程终止时所有线程会被强制终止

分离线程结束后,其占用的资源(如线程栈、控制块)会自动释放,无需当前线程 join 回收。并且 detach 过后的线程本来就不能再进行 join。

1
2
#include <pthread.h>
int pthread_detach(pthread_t thread);

参数

  • thread:要分离的线程 ID。

返回值:成功返回 0,失败返回错误码。

分离线程的适用场景:"即发即忘"任务

  • 日志记录:将日志写入操作放在分离线程中
  • 定期清理任务:内存清理、缓存刷新等
  • 异步通知:发送通知而不阻塞主线程
  • 监控任务:定期检查系统状态

总结pthread_detach 创建的线程可以理解为 在后台运行,但仍然 与进程同生共死,并且 无法再与主线程进行同步或通信



3 Linux C 多线程核心函数使用示例

3.1 pthread_createpthread_join 示例

创建了两个线程,执行线程函数 foo()。主线程执行的是 main(),通过 pthread_join 等待两个子线程执行完毕。两个子线程并发执行,会产生交替打印的效果。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

// 线程函数
void* foo(void* arg) {
int arg_num = *((int*)arg); // 传递参数转换为整数
for (int i = 0; i < 3; i ++ ) {
printf("Thread %d is working... %d/3\n", arg_num, i);
sleep(1);
}
// 退出并返回一个值
int* ret_val = malloc(sizeof(int));
*ret_val = arg_num * 233;
pthread_exit((void*)ret_val); // 等价于 return ret_val
}

int main() {
pthread_t tid1, tid2;
int arg1 = 1, arg2 = 2;
void* ret1;
void* ret2;

// 创建线程 1
if (pthread_create(&tid1, NULL, foo, &arg1) != 0) {
perror("pthread_create 1 error");
exit(1);
}

// 创建线程 2
if (pthread_create(&tid2, NULL, foo, &arg2) != 0) {
perror("pthread_create 2 error");
exit(1);
}

// 主线程等待两个子线程结束
printf("Main thread is waiting for threads to finish...\n");

// 等待子线程 1 结束
if (pthread_join(tid1, &ret1) != 0) {
perror("pthread_join 1 error");
exit(1);
}
printf("Thread 1 returned: %d\n", *((int*)ret1));
free(ret1);

// 等待子线程 2 结束
if (pthread_join(tid2, &ret2) != 0) {
perror("pthread_join 2 error");
exit(1);
}
printf("Thread 2 returned: %d\n", *((int*)ret2));
free(ret2);

printf("Main thread exits.\n");

return 0;
}


3.2 pthread_detach 示例

创建一个分离线程,主线程不等待它。分离线程可以被看作在后台运行,不再受到主线程的控制。分离线程的资源会在主进程退出前被自动回收

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
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

void* foo(void* arg) {
printf("Detached thread started.\n");
sleep(2); // 模拟工作
printf("Detached thread finished. It will auto-cleanup.\n");
pthread_exit(NULL);
}

int main() {
pthread_t tid;

if (pthread_create(&tid, NULL, foo, NULL) != 0) {
perror("pthread_create failed");
exit(1);
}

// 立即分离新创建的线程
if (pthread_detach(tid) != 0) {
perror("pthread_detach failed");
exit(1);
}

printf("Main thread continues. It will not wait for the detached thread.\n");
sleep(3); // 主线程稍作睡眠,以便观察到分离线程的输出
// 在实际代码中,主线程通常不会这样睡眠
printf("Main thread exits.\n");
// 分离线程的资源会在主进程退出前被自动回收
return 0;
}



4 pthread_cancel 函数及使用

4.1 pthread_cancel 函数

pthread_cancel向指定的线程发送一个取消请求(cancellation request)。它不会立即强制终止线程,而是礼貌地“请求”目标线程终止自己。目标线程是否会终止、何时终止,取决于其自身的取消性状态(cancellation state)和类型(cancellation type)。

1
2
#include <pthread.h>
int pthread_cancel(pthread_t thread);

参数

thread:要取消的目标线程的 ID。

返回值

  • 成功返回 0。
  • 失败返回一个非零的错误码(注意:不是设置 errno)。

4.2 线程的取消机制(状态和类型)

线程对于取消请求的响应行为是由两个属性共同决定的:

  1. 取消状态 (Cancellation State)

    • PTHREAD_CANCEL_ENABLE (默认值)
      • 线程可以接收取消请求。这是新创建线程的默认状态。
    • PTHREAD_CANCEL_DISABLE
      • 线程忽略所有接收到的取消请求。取消请求会被挂起,直到线程的取消状态再次变为 ENABLE
  2. 取消类型 (Cancellation Type)

    • PTHREAD_CANCEL_DEFERRED (默认值)
      • 延迟取消。线程不会立即终止,只有当它执行到一个取消点 (cancellation point) 时,取消请求才会被处理。这是新创建线程的默认类型。
    • PTHREAD_CANCEL_ASYNCHRONOUS
      • 异步取消。线程可以在任何时间点被立即取消,取消点不再是必须的。不过非常危险,因为它可能发生在线程正在修改共享数据或持有锁的时候,极易导致状态不一致或死锁。
  3. 取消点 (Cancellation Point)

    取消点是一些可能引起阻塞的库函数。当线程(处于 ENABLEDEFERRED 状态)执行这些函数时,会 检查是否有挂起的取消请求,如果有,就会开始处理取消操作

    常见的取消点包括:

    • 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
2
3
4
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
void pthread_testcancel(void);
  • pthread_setcancelstate(): 设置取消状态(ENABLE 或 DISABLE),并可选择保存旧状态。

  • pthread_setcanceltype(): 设置取消类型(DEFERRED 或 ASYNCHRONOUS),并可选择保存旧类型。

  • pthread_testcancel(): 如果当前线程有挂起的取消请求,则此函数会触发取消操作,否则直接返回。


4.4 线程清理处理程序

当一个线程被成功取消时,为了确保资源(如已分配的堆内存、已打开的锁等)能够被正确释放,必须使用清理处理程序

1
2
3
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
  • pthread_cleanup_push(): 将一个清理函数 routine 压入当前线程的清理栈。当线程被取消或调用 pthread_exit() 时,栈中的所有清理函数会以相反的顺序(后进先出)被调用。arg 是传递给清理函数的参数。
  • pthread_cleanup_pop(): 从清理栈中弹出一个清理函数。如果 execute 参数非零,则执行该清理函数;如果为 0,则只是弹出而不执行。

注意

  • pushpop 必须成对出现,且在同一个代码块内(同一个函数内、同一对花括号 {} 内)。
  • 线程正常通过 return 结束时,清理处理程序不会被调用。

4.5 pthread_cancel 示例

创建一个线程,允许它被取消,并设置清理函数来安全地释放资源。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

// 清理函数
void cleanup_handler(void *arg) {
// 释放资源,例如:解锁、关闭文件、释放堆内存等
printf("Cleanup: Freeing allocated memory at %p\n", arg);
free(arg);
}

// 线程函数
void* foo(void *arg) {
int oldstate;
// 允许此线程被取消 (默认就是ENABLE,这里显式设置一下)
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldstate);

// 分配一些资源(模拟)
int *data = (int*)malloc(sizeof(int));
*data = 42;
printf("Thread allocated memory at %p\n", (void*)data);

// 注册清理函数
pthread_cleanup_push(cleanup_handler, data);

// 模拟工作:循环并检查取消点
for (int i = 0; i < 10; i++) {
printf("Thread working... %d\n", i);
sleep(1); // sleep是一个取消点
// 如果线程在计算(没有取消点),应使用:
// pthread_testcancel();
}

// 弹出清理函数但不执行(因为正常结束,资源在下面自己释放)
pthread_cleanup_pop(0);

// 线程正常结束,自己释放资源
printf("Thread finishing normally, freeing memory.\n");
free(data);
return NULL;
}

int main() {
pthread_t tid;

printf("Main: Creating thread.\n");
if (pthread_create(&tid, NULL, foo, NULL) != 0) {
perror("pthread_create failed");
exit(EXIT_FAILURE);
}

// 让新线程运行一会儿
sleep(3);

printf("Main: Cancelling the thread.\n");
if (pthread_cancel(tid) != 0) {
perror("pthread_cancel failed");
}

// 等待线程结束(无论是被取消还是正常结束)
printf("Main: Joining the thread.\n");
if (pthread_join(tid, NULL) != 0) {
perror("pthread_join failed");
}

printf("Main: Thread has ended.\n");
return 0;
}


5 本篇与 C++ 多线程的区别

本篇内容对应 C++ 多线程的内容可见:C++ 多线程」并发与多线程「C++ 多线程」join(), detach(), joinable()「C++ 多线程」std::thread 线程函数参数传递 等。

在 C++ 中,没有提供直接强制终止或取消另一个线程的函数。主要是出于安全性和可靠性的考虑:

  1. 资源泄漏风险:强制终止线程可能导致:

    • 内存泄漏(分配的内存未释放)
    • 文件描述符泄漏(打开的文件未关闭)
    • 互斥锁未解锁(导致死锁)
    • 其他系统资源未正确释放
  2. 数据一致性风险:线程可能在修改共享数据时被终止,导致数据结构处于不一致状态

  3. 可预测性问题:无法确定线程在何时何地被终止,使程序行为难以预测和调试

如果要在 C++ 使用线程终止/取消的函数,通常会采用结合 标志变量 或者 条件变量(condition_variable)等来安全实现。


「Linux 系统编程」线程的概念与控制
https://marisamagic.github.io/2025/09/17/20250917/
作者
MarisaMagic
发布于
2025年9月17日
许可协议