一、为什么需要线程?
1.1 回顾多进程的缺点
我们之前学习了多进程服务器:父进程 fork 出子进程来处理客户端请求。这种方式虽然能实现并发,但存在一些问题:
- 资源开销大:每个进程都有独立的地址空间,创建和切换进程的成本较高。
- 通信麻烦:进程间通信(IPC)需要管道、共享内存等机制,比较繁琐。
- 数据共享困难:父子进程虽然 fork 时复制了数据,但之后各自独立,要共享数据必须用 IPC。
1.2 线程的优势
线程 (Thread)是进程内的一个执行流,它共享进程的地址空间(代码段、数据段、堆等),但拥有独立的栈和寄存器上下文。
| 对比项 | 进程 | 线程 |
|---|---|---|
| 地址空间 | 独立 | 共享(除了栈) |
| 创建开销 | 大 | 小 |
| 切换开销 | 大 | 小 |
| 数据共享 | 需要 IPC | 直接访问全局变量 |
| 并发性 | 多进程可多核 | 多线程也可多核 |
| 稳定性 | 一个进程崩溃不影响其他 | 一个线程崩溃可能导致整个进程崩溃 |
因此,在需要大量并发且频繁共享数据的场景下(如 Web 服务器、GUI 程序),线程是更合适的选择。
二、线程的基本概念
- 线程 ID :每个线程有一个唯一的标识符(
pthread_t类型),类似于进程的 PID。 - 线程主函数 :线程开始执行的地方,函数原型为
void* thread_func(void* arg)。 - 线程的终止 :线程函数返回、调用
pthread_exit()或被取消。 - 线程的回收 :类似进程的
wait,需要调用pthread_join等待线程结束并回收资源,否则可能造成僵尸线程(类似僵尸进程)。
三、创建线程:pthread_create
3.1 函数原型
c
#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(void*)。 - arg:传递给线程主函数的参数(可以是任意指针)。
- 返回值:成功返回 0,失败返回非零错误码(不是 -1)。
3.2 线程主函数格式
c
void* my_thread_func(void *arg) {
// 线程执行的代码
return (void*)some_ptr; // 返回值可以通过 pthread_join 获得
}
3.3 参数传递
由于 arg 是 void*,可以传递任意类型:
- 传递基本类型:将值强转为
void*(注意数据长度)。 - 传递结构体:传结构体指针。
- 传递多个参数:封装成结构体。
四、等待线程结束:pthread_join
4.1 为什么需要 join?
- 主线程结束时会终止整个进程,所有子线程也会被强制结束。
- 为了避免主线程过早退出,需要等待子线程完成。
- 同时可以获取子线程的返回值,并回收线程资源(防止资源泄漏)。
4.2 函数原型
c
int pthread_join(pthread_t thread, void **retval);
- thread:要等待的线程 ID。
- retval :二级指针,用于接收线程主函数的返回值(如果不需要可传
NULL)。 - 返回值:成功返回 0,失败返回错误码。
4.3 注意
pthread_join会阻塞调用线程,直到目标线程终止。- 一个线程只能被 join 一次,多次 join 行为未定义。
- 如果线程已经终止,
pthread_join会立即返回。
五、完整示例:创建线程并等待返回值
下面是一个完整的示例,主线程创建一个子线程,子线程循环打印消息,最后返回一个字符串,主线程打印该字符串并释放内存。
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
// 线程主函数
void* thread_main(void *arg) {
int i;
int cnt = *((int*)arg); // 获取传入的参数(循环次数)
char *msg = (char*)malloc(sizeof(char) * 50);
strcpy(msg, "Hello, I'm thread~ \n");
for (i = 0; i < cnt; i++) {
sleep(1); // 模拟耗时工作
puts("running thread");
}
return (void*)msg; // 返回字符串指针
}
int main() {
pthread_t t_id;
int thread_param = 5;
void *thr_ret;
// 1. 创建线程
if (pthread_create(&t_id, NULL, thread_main, (void*)&thread_param) != 0) {
perror("pthread_create");
return -1;
}
// 2. 等待线程结束,并获取返回值
if (pthread_join(t_id, &thr_ret) != 0) {
perror("pthread_join");
return -1;
}
// 3. 打印线程返回的消息
printf("Thread return message: %s\n", (char*)thr_ret);
free(thr_ret); // 释放线程内部分配的内存
return 0;
}
5.1 编译与运行
由于 pthread 库不是默认链接的,编译时需要加上 -pthread 选项:
bash
gcc -pthread thread_example.c -o thread_example
./thread_example
输出:
running thread
running thread
running thread
running thread
running thread
Thread return message: Hello, I'm thread~
5.2 代码详解
| 行号 | 解释 |
|---|---|
pthread_create(&t_id, NULL, thread_main, (void*)&thread_param) |
创建线程,参数 thread_param 的地址传给线程函数。 |
thread_main 中的 int cnt = *((int*)arg) |
解引用获取传入的整数值。 |
malloc 分配字符串 |
线程内部分配内存,返回给主线程,主线程负责释放。 |
pthread_join(t_id, &thr_ret) |
主线程等待子线程结束,thr_ret 接收返回值。 |
free(thr_ret) |
释放子线程中 malloc 的内存,避免内存泄漏。 |
六、常见问题与注意事项
6.1 编译时链接错误
错误 :undefined reference to pthread_create'
原因 :未链接 pthread 库。
解决 :编译时加上 -pthread 选项。
6.2 主线程过早退出
c
// 错误示例
int main() {
pthread_t tid;
pthread_create(&tid, NULL, func, NULL);
// 没有 pthread_join,主线程直接退出 → 整个进程结束,子线程被强制终止
}
解决 :调用 pthread_join 等待子线程,或调用 pthread_exit(NULL) 让主线程退出而进程继续(但通常不推荐)。
6.3 传递局部变量作为线程参数
c
// 危险示例
void* func(void *arg) {
int *p = (int*)arg;
printf("%d\n", *p); // p 指向的可能是主线程的局部变量,但主线程可能已退出或变量失效
}
int main() {
int x = 10;
pthread_create(&tid, NULL, func, &x);
pthread_join(tid, NULL); // 这里 x 还在作用域内,安全
return 0;
}
注意 :如果主线程在子线程使用参数之前就退出了(或局部变量被销毁),则子线程会访问无效内存。因此要么用 pthread_join 等待,要么将参数放在堆上(malloc)。
6.4 返回值内存管理
- 线程主函数返回的指针通常指向静态数据 或堆上分配的内存。
- 如果返回局部变量的地址,该内存在线程函数退出后会被释放,造成悬空指针。
- 最佳实践 :使用
malloc分配返回值,调用pthread_join的线程负责free。
6.5 线程 ID 的类型
pthread_t 可能是一个整数或结构体,不能直接当作整数打印(除非用 %lu 并强制转换,但不可移植)。调试时可使用 pthread_self() 获取当前线程 ID,并打印指针值。
七、总结
7.1 核心函数速查
| 函数 | 作用 |
|---|---|
pthread_create(&tid, NULL, func, arg) |
创建新线程 |
pthread_join(tid, &retval) |
等待线程结束并获取返回值 |
pthread_self() |
获取当前线程的 ID |
pthread_exit(retval) |
主动终止当前线程(类似 return) |
7.2 线程 vs 进程
| 项目 | 进程 | 线程 |
|---|---|---|
| 数据共享 | 复杂(需要 IPC) | 简单(直接访问全局变量) |
| 创建/切换开销 | 大 | 小 |
| 崩溃影响 | 其他进程不受影响 | 整个进程可能崩溃 |
| 同步需求 | 较少(数据独立) | 较多(需锁保护共享数据) |