C高级:关于多线程编程的学习记录
多线程是 Linux 系统编程中实现并发执行的核心手段。相比多进程方案,线程更轻量、切换更快、共享数据更方便。本文从线程的基本概念讲起,对比线程与进程的区别,然后深入讲解 POSIX 线程库(pthreads)中最常用的三个函数:pthread_create()、pthread_join() 和 pthread_detach(),每个参数逐一拆解,配合完整可编译的示例代码,以下是我在职坐标的学习记录:
一、线程是什么
1.1 基本概念
线程(Thread)是进程中的一个执行单元,是 CPU 调度和分派的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源(地址空间、文件描述符、全局变量等),但每个线程拥有自己独立的栈空间、寄存器状态和程序计数器(PC)。
可以这样理解:进程就像一家公司,拥有办公场地、设备、资金等资源;而线程就是公司里的员工,每个人有自己的工位(栈)和手头的工作(寄存器),但共用公司的会议室、打印机等公共设施(堆、全局变量、文件描述符)。
1.2 线程的组成
每个线程都拥有以下独立的资源:
- 线程 ID(TID):在进程范围内唯一标识该线程。
- 程序计数器(PC):记录当前执行到指令的哪一条。
- 寄存器集合:保存线程运行时的中间状态。
- 栈(Stack):用于存放局部变量、函数调用帧、返回地址等。每个线程有独立的栈,默认大小通常为 8MB(可通过线程属性修改)。
而以下资源是同一进程内所有线程共享的:
- 代码段(Text Segment)
- 数据段(Data Segment)和 BSS 段
- 堆(Heap)
- 文件描述符表
- 信号处理函数
- 当前工作目录
- 用户 ID 和组 ID
1.3 为什么需要线程
使用多线程的主要动机包括:
- 提高并发性:多个线程可以同时执行不同的任务(在多核 CPU 上真正并行,在单核 CPU 上通过时间片轮转实现并发)。
- 降低开销:创建线程的开销远小于创建进程(不需要复制地址空间和文件描述符表)。
- 共享数据方便:同一进程内的线程可以直接通过全局变量或堆内存交换数据,不需要管道、共享内存等 IPC 机制。
- 响应性:在 GUI 程序中,可以把耗时操作放到后台线程,保持主线程(UI 线程)的响应。
二、线程与进程的区别
2.1 核心对比
| 对比维度 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 定义 | 操作系统资源分配的基本单位 | CPU 调度和执行的基本单位 |
| 地址空间 | 每个进程拥有独立的虚拟地址空间 | 同一进程内的线程共享地址空间 |
| 资源开销 | 创建和销毁开销大(需要复制页表、文件描述符表等) | 创建和销毁开销小 |
| 切换开销 | 上下文切换开销大(需要切换页表、刷新 TLB) | 上下文切换开销小(同一地址空间内切换) |
| 数据共享 | 进程间数据隔离,需要 IPC 机制(管道、共享内存等) | 线程间直接共享全局变量和堆内存 |
| 同步问题 | 不存在数据竞争(天然隔离) | 存在数据竞争,需要互斥锁、条件变量等同步机制 |
| 健壮性 | 一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致整个进程挂掉 |
| 创建方式 | fork() / vfork() |
pthread_create() |
| 通信方式 | 管道、消息队列、共享内存、Socket、信号 | 直接读写共享变量(需加锁)、条件变量 |
2.2 什么时候用线程,什么时候用进程
- 用线程:多个任务需要频繁共享数据、对性能要求高、任务之间关联紧密(如 Web 服务器为每个客户端连接创建一个工作线程)。
- 用进程:任务之间相互独立、需要更好的隔离性和健壮性、不需要频繁共享数据(如 Shell 管道中的多个命令)。
三、POSIX 线程库(pthreads)
Linux 下的标准线程库是 pthreads(POSIX threads),它是一组遵循 POSIX 标准的 C 语言 API。使用时需要包含头文件 <pthread.h>,并在编译时链接线程库 -lpthread。
#include <pthread.h>
// 编译时加 -lpthread
// gcc program.c -o program -lpthread
线程的核心类型是 pthread_t,它是一个不透明的类型(在某些系统上是 unsigned long),用于标识一个线程。
四、pthread_create() —— 创建线程
4.1 函数原型
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
4.2 函数作用
pthread_create() 创建一个新的线程,新线程从 start_routine 函数开始执行。调用成功后,新线程与调用线程(父线程)并发运行。
4.3 参数详解
| 参数 | 类型 | 方向 | 说明 |
|---|---|---|---|
thread |
pthread_t * |
输出 | 指向 pthread_t 类型变量的指针。函数成功返回后,这个变量会被填充为新线程的 线程 ID(TID),后续通过它来引用这个线程(如 pthread_join()、pthread_detach())。 |
attr |
const pthread_attr_t * |
输入 | 指向线程属性对象的指针,用于设置线程的各种属性(栈大小、调度策略、分离状态等)。通常传 NULL,表示使用默认属性。如果需要自定义,先用 pthread_attr_init() 初始化属性对象。 |
start_routine |
void *(*)(void *) |
输入 | 新线程的入口函数(线程开始执行的函数)。函数签名必须是 void *func(void *arg):接受一个 void * 参数,返回一个 void * 指针。返回值可以通过 pthread_join() 被其他线程获取。 |
arg |
void * |
输入 | 传递给 start_routine 的参数。可以是任意类型的数据,通过强制类型转换传入。如果不需要传参,可以传 NULL。如果需要传递多个参数,通常的做法是将参数封装成一个结构体,然后传递结构体的指针。 |
关于 start_routine 的参数和返回值:
// 线程入口函数的标准形式
void *thread_func(void *arg) {
// arg 就是 pthread_create 的第四个参数
int *num = (int *)arg; // 假设传入的是 int 指针
printf("收到参数: %d\n", *num);
// 返回值可以通过 pthread_join 获取
return NULL; // 或者返回一个指针
}
关于 arg 的常见陷阱:
// ❌ 错误:传递局部变量地址(线程执行时局部变量可能已失效)
void create_thread_bad() {
int value = 42;
pthread_t tid;
pthread_create(&tid, NULL, thread_func, &value);
// 函数返回后,value 的栈空间被回收,线程可能读到垃圾值
}
// ✅ 正确:使用堆分配或静态/全局变量
void create_thread_good() {
int *value = malloc(sizeof(int));
*value = 42;
pthread_t tid;
pthread_create(&tid, NULL, thread_func, value);
}
4.4 返回值
- 成功:返回
0。 - 失败:返回一个正的错误码(不是
-1,这一点和大多数系统调用不同)。常见的错误码包括:EAGAIN:系统资源不足(如线程数达到上限)。EINVAL:attr参数无效。EPERM:没有权限设置指定的调度策略。
注意:pthread_create() 失败时不会设置 errno,而是直接返回错误码。因此不能用 perror(),而应该用 strerror() 来打印错误信息:
int ret = pthread_create(&tid, NULL, func, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_create failed: %s\n", strerror(ret));
}
4.5 示例代码
基本用法:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 线程入口函数
void *print_hello(void *arg) {
char *name = (char *)arg;
printf("Hello from thread, name = %s\n", name);
printf("线程 ID: %lu\n", (unsigned long)pthread_self());
return NULL;
}
int main() {
pthread_t tid;
// 创建线程,使用默认属性,传入字符串参数
int ret = pthread_create(&tid, NULL, print_hello, "Worker");
if (ret != 0) {
printf("创建线程失败: %s\n", strerror(ret));
return 1;
}
printf("主线程: 已创建子线程,TID = %lu\n", (unsigned long)tid);
// 等待子线程结束(下一节详细讲解)
pthread_join(tid, NULL);
printf("主线程: 子线程已结束\n");
return 0;
}
传递多个参数(结构体方式):
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
// 定义参数结构体
struct thread_args {
int id;
char *name;
double score;
};
void *worker(void *arg) {
struct thread_args *args = (struct thread_args *)arg;
printf("ID: %d, Name: %s, Score: %.2f\n",
args->id, args->name, args->score);
// 释放参数内存
free(args);
return NULL;
}
int main() {
pthread_t tid;
// 动态分配参数结构体
struct thread_args *args = malloc(sizeof(struct thread_args));
args->id = 1;
args->name = "Alice";
args->score = 95.5;
pthread_create(&tid, NULL, worker, args);
pthread_join(tid, NULL);
return 0;
}
五、pthread_join() —— 等待线程结束并回收资源
5.1 函数原型
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
5.2 函数作用
pthread_join() 使调用线程阻塞等待指定的线程结束。当目标线程终止后,pthread_join() 会回收该线程的资源(栈、线程描述符等),并可选地获取目标线程的返回值。
这个函数的角色类似于进程编程中的 waitpid()——如果一个线程结束后没有被 join 也没有被 detach,它就会变成一个"僵尸线程",持续占用系统资源。因此,每个默认创建的线程都必须通过 pthread_join() 或 pthread_detach() 来回收。
5.3 参数详解
| 参数 | 类型 | 方向 | 说明 |
|---|---|---|---|
thread |
pthread_t |
输入 | 要等待的目标线程的 TID,即 pthread_create() 填充的那个值。注意这里传的是值,不是指针。 |
retval |
void ** |
输出 | 一个指向指针的指针,用于接收目标线程的返回值。目标线程通过 return 语句或 pthread_exit() 返回的值,会存储到 *retval 中。如果不需要获取返回值,可以传 NULL。 |
关于 retval 的使用:
void *retval;
pthread_join(tid, &retval);
// 如果子线程 return (void*)42; 则 retval 的值为 42
// 如果不需要返回值
pthread_join(tid, NULL);
重要注意事项:
retval不能指向目标线程的局部变量(栈上变量),因为线程结束后其栈空间会被释放。返回值应该是堆上的地址、静态变量地址,或者直接将整数值强转为指针。- 只能对默认属性(joinable) 创建的线程调用
pthread_join()。如果线程已经被设置为分离状态(detached),调用pthread_join()会返回EINVAL错误。 - 同一个线程只能被 join 一次。对已经 join 过的线程再次调用
pthread_join()会导致未定义行为(通常返回ESRCH或崩溃)。 pthread_join()是阻塞调用——调用线程会一直等待,直到目标线程结束。如果需要非阻塞方式,可以使用pthread_tryjoin_np()(Linux 特有,非标准)。
5.4 返回值
- 成功:返回
0,retval被填充为目标线程的返回值。 - 失败:返回正的错误码。常见错误码:
ESRCH:找不到指定 TID 的线程。EINVAL:目标线程是分离状态(detached),或者已经被其他线程 join。EDEADLK:检测到死锁(如线程试图 join 自己)。
5.5 示例代码
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
// 线程函数:计算 1~n 的和,通过返回值传出结果
void *sum_worker(void *arg) {
int n = *(int *)arg;
long long sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
// 注意:不能返回局部变量的地址
// 方案 1:将结果放到堆上
long long *result = malloc(sizeof(long long));
*result = sum;
return result;
}
int main() {
pthread_t tid;
int n = 1000000;
pthread_create(&tid, NULL, sum_worker, &n);
void *retval;
pthread_join(tid, &retval);
long long sum = *(long long *)retval;
printf("1 ~ %d 的和 = %lld\n", n, sum);
free(retval); // 释放堆上的返回值
return 0;
}
多线程式示例(创建多个线程并分别 join):
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define NUM_THREADS 4
struct task_arg {
int thread_id;
int start;
int end;
};
void *partial_sum(void *arg) {
struct task_arg *ta = (struct task_arg *)arg;
long long sum = 0;
for (int i = ta->start; i <= ta->end; i++) {
sum += i;
}
printf("线程 %d: 计算 %d ~ %d 的和 = %lld\n",
ta->thread_id, ta->start, ta->end, sum);
long long *result = malloc(sizeof(long long));
*result = sum;
return result;
}
int main() {
pthread_t threads[NUM_THREADS];
struct task_arg args[NUM_THREADS];
int range = 250000;
// 创建 4 个线程,每个线程计算一段区间的和
for (int i = 0; i < NUM_THREADS; i++) {
args[i].thread_id = i;
args[i].start = i * range + 1;
args[i].end = (i + 1) * range;
pthread_create(&threads[i], NULL, partial_sum, &args[i]);
}
// 依次 join 所有线程,收集结果
long long total = 0;
for (int i = 0; i < NUM_THREADS; i++) {
void *retval;
pthread_join(threads[i], &retval);
total += *(long long *)retval;
free(retval);
}
printf("总和 = %lld\n", total);
return 0;
}
六、pthread_detach() —— 分离线程(自动回收资源)
6.1 函数原型
#include <pthread.h>
int pthread_detach(pthread_t thread);
6.2 函数作用
pthread_detach() 将指定的线程设置为分离状态(detached)。处于分离状态的线程在终止时会自动释放自己的所有资源(栈、线程描述符等),不需要其他线程来 join 它。
这个函数的使用场景是:当你创建了一个"后台任务"线程,只关心它执行任务,不关心它的返回值,也不想在它结束后手动 join 时,就可以将它分离。
6.3 参数详解
| 参数 | 类型 | 方向 | 说明 |
|---|---|---|---|
thread |
pthread_t |
输入 | 要设置为分离状态的目标线程的 TID。可以是其他线程的 TID,也可以在线程内部调用 pthread_detach(pthread_self()) 将自己分离。 |
6.4 分离线程 vs 可连接线程
| 对比维度 | 可连接线程(Joinable,默认) | 分离线程(Detached) |
|---|---|---|
| 资源回收 | 必须由其他线程调用 pthread_join() 回收 |
线程终止时自动回收 |
| 获取返回值 | 可以通过 pthread_join() 获取 |
不能获取返回值 |
| 能否 join | 可以 | 不可以(调用 pthread_join() 会返回 EINVAL) |
| 适用场景 | 需要等待线程结果、需要获取返回值 | 后台任务、不关心结果、不希望管理生命周期 |
两种设置分离状态的方式:
// 方式 1:创建后调用 pthread_detach()
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
pthread_detach(tid);
// 方式 2:创建时通过线程属性设置
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_t tid;
pthread_create(&tid, &attr, worker, NULL);
pthread_attr_destroy(&attr); // 用完销毁属性对象
// 方式 3:在线程内部将自己分离
void *worker(void *arg) {
pthread_detach(pthread_self());
// 执行任务...
return NULL; // 线程结束时自动回收
}
6.5 返回值
- 成功:返回
0。 - 失败:返回正的错误码。常见错误码:
ESRCH:找不到指定 TID 的线程。EINVAL:目标线程已经是分离状态,或者已经被 join。
6.6 注意事项
- 一个线程只能被 detach 一次。对已经处于分离状态的线程再次调用
pthread_detach()会返回EINVAL。 - 分离状态的线程不能被 join。如果需要对一个已经 detach 的线程调用
pthread_join(),会失败。 - 一旦线程被分离,就没有办法将它恢复为可连接状态。
6.7 示例代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 后台日志线程
void *logger(void *arg) {
// 将自己设置为分离状态
pthread_detach(pthread_self());
for (int i = 0; i < 5; i++) {
printf("[Logger] 正在记录日志... (%d/5)\n", i + 1);
sleep(1);
}
printf("[Logger] 日志记录完成,线程将自动回收资源\n");
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, logger, NULL);
// 主线程继续做其他事情,不需要 join logger
printf("[Main] 后台日志线程已启动,主线程继续执行...\n");
// 主线程做一些工作
sleep(7);
printf("[Main] 主线程结束\n");
// logger 线程会在结束时自动回收资源
return 0;
}
七、pthread_exit() —— 线程退出
虽然不在题目要求内,但作为线程编程的基础知识,这里简要提一下。
#include <pthread.h>
void pthread_exit(void *retval);
线程可以通过三种方式终止:
- 从入口函数
return:返回一个void *值作为线程的退出状态。 - 被其他线程
pthread_cancel():强制终止目标线程。 - 调用
pthread_exit():主动终止自己,并设置返回值。
void *worker(void *arg) {
// 做一些工作...
// 方式 1:直接 return
// return NULL;
// 方式 2:调用 pthread_exit
pthread_exit(NULL); // 等价于 return NULL
}
注意:pthread_exit() 不会关闭进程打开的文件描述符,也不会释放 malloc 分配的内存(除非在线程内部手动释放)。它只是终止当前线程的执行。如果在 main() 中调用 pthread_exit(),主线程会终止,但其他线程仍然可以继续运行,直到所有线程都退出后进程才会真正结束。
八、三个函数的对比总结
| 函数 | 作用 | 一句话记忆 |
|---|---|---|
pthread_create() |
创建一个新线程并开始执行 | create a new thread |
pthread_join() |
阻塞等待线程结束,回收资源并获取返回值 | join(汇合)等待线程完成 |
pthread_detach() |
设置线程为分离状态,结束后自动回收资源 | detach(分离)让线程自己负责回收 |
线程生命周期总结:
┌─── pthread_join() ──→ 回收资源,获取返回值
│
pthread_create() ──→ 线程运行中 ──→ 线程结束
│
└─── pthread_detach() ──→ 自动回收资源(无法获取返回值)
九、常见问题 FAQ
Q1:pthread_join() 和 pthread_detach() 可以同时使用吗?
不可以。一个线程要么被 join,要么被 detach,二选一。对同一个线程同时做两件事会导致错误。
Q2:不 join 也不 detach 会怎样?
线程终止后会变成"僵尸线程"(类似僵尸进程),持续占用系统资源(线程描述符、栈空间等)。如果反复创建线程而不回收,最终会导致资源耗尽。
Q3:主线程退出,子线程会怎样?
如果主线程调用 return 或 exit(),整个进程会终止,所有线程都会被强制结束。如果主线程调用 pthread_exit(),主线程自己退出,但子线程可以继续运行,直到所有线程都退出后进程才终止。
Q4:线程函数中可以调用 exit() 吗?
不建议。exit() 会终止整个进程(包括所有线程),而不仅仅是当前线程。应该使用 pthread_exit() 或 return 来终止单个线程。
十、编译说明
所有使用 pthreads 的程序在编译时都需要链接 -lpthread(或 -pthread):
# 方式 1
gcc program.c -o program -lpthread
# 方式 2(推荐,自动处理链接和宏定义)
gcc program.c -o program -pthread
浙公网安备 33010602011771号