2. POSIX线程库入门:pthread_create、pthread_join、pthread_exit函数详解
好,咱们正式开始动手写多线程代码了。
POSIX线程库,也就是常说的pthread,是Linux和Unix系统下最正统的多线程接口。我当年刚接触它的时候,觉得函数名又长又难记,但用久了你会发现——这套API设计得其实很规整,每个参数都有它的道理。
今天咱们就啃下三个最核心的函数:pthread_create、pthread_join、pthread_exit。说白了,一个管生,一个管死,一个管善后。
2.1 线程的创建:pthread_create
创建线程,用的是pthread_create。它的原型长这样:
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg);
四个参数,我一个个说。
- thread:输出参数。线程创建成功后,这里会填上线程ID。你可以把它理解成线程的“身份证号”。
- attr:线程属性。一般传NULL,用默认属性就行。我刚开始学的时候,总想在这里搞点花样,后来发现99%的场景默认值就够了。
- start_routine:线程要执行的函数。注意它的签名——
void *(*)(void *)。参数和返回值都是void *,这是pthread的统一约定。 - arg:传给线程函数的参数。你想传什么就传什么,结构体、整数、字符串都行,但记得要强转成
void *。
返回值:成功返回0,失败返回错误码。
重要:pthread函数失败时不会设置errno,而是直接返回错误码。所以别用perror去打印错误,要用strerror_r。
来个最简单的例子:
#include <stdio.h>
#include <pthread.h>
void *print_message(void *arg) {
char *msg = (char *)arg;
printf("线程说:%s\n", msg);
return NULL;
}
int main() {
pthread_t tid;
char *hello = "Hello from thread!";
int ret = pthread_create(&tid, NULL, print_message, (void *)hello);
if (ret != 0) {
fprintf(stderr, "创建线程失败,错误码:%d\n", ret);
return 1;
}
// 这里要等一下,否则主线程退出后子线程可能来不及执行
pthread_join(tid, NULL);
return 0;
}
编译的时候别忘了加-lpthread:
gcc -o demo demo.c -lpthread
小技巧:我个人习惯把线程函数声明为static,限制它的作用域。这样代码结构更清晰,别人一看就知道这个函数是给线程用的。
2.2 线程的等待:pthread_join
主线程创建了子线程,然后呢?
如果不做任何处理,主线程可能先跑完,整个进程就退出了。子线程还没开始干活就被强行终止了。这显然不是我们想要的。
pthread_join就是用来解决这个问题的。它会阻塞调用线程,直到目标线程结束。
int pthread_join(pthread_t thread, void **retval);
- thread:要等待的线程ID。
- retval:输出参数。如果线程通过
return或pthread_exit返回了一个值,这里就能拿到。不关心返回值就传NULL。
我举个例子,看看怎么拿到线程的返回值:
#include <stdio.h>
#include <pthread.h>
void *compute_sum(void *arg) {
int n = *(int *)arg;
int *result = malloc(sizeof(int));
*result = n * (n + 1) / 2;
return (void *)result;
}
int main() {
pthread_t tid;
int n = 100;
pthread_create(&tid, NULL, compute_sum, &n);
void *retval;
pthread_join(tid, &retval);
int *sum = (int *)retval;
printf("1到%d的和是:%d\n", n, *sum);
free(sum);
return 0;
}
注意:pthread_join只能调用一次。对一个线程多次调用pthread_join,行为是未定义的。我曾经在项目里踩过这个坑——一个线程被两个地方同时join,结果程序直接崩溃了。
2.3 线程的退出:pthread_exit
线程退出有两种方式:
- 在线程函数里执行
return语句。 - 调用
pthread_exit函数。
两者效果基本一样,但有一个细微差别:
return会返回到调用者,返回值通过pthread_join的第二个参数拿到。pthread_exit直接终止当前线程,返回值通过参数传递。
void pthread_exit(void *retval);
看个例子:
#include <stdio.h>
#include <pthread.h>
void *worker(void *arg) {
printf("线程开始工作...\n");
// 做一些处理
pthread_exit((void *)42);
// 这里的代码永远不会执行
printf("这行不会打印\n");
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
void *ret;
pthread_join(tid, &ret);
printf("线程退出码:%ld\n", (long)ret);
return 0;
}
关键点:pthread_exit不会执行任何清理操作。如果你在线程里malloc了内存,记得在调用pthread_exit之前free掉。否则,内存泄漏就找上门了。
2.4 三个函数的协作关系
这三个函数配合起来,构成了pthread线程生命周期管理的核心。我画了一张图,帮你理清它们的关系:
流程其实很简单:
- 主线程调用
pthread_create,子线程诞生。 - 子线程执行自己的任务,主线程可以继续干别的事。
- 子线程通过
return或pthread_exit退出。 - 主线程调用
pthread_join,回收子线程资源,拿到返回值。
2.5 避坑指南
这几个函数看着简单,但用起来有不少坑。我把自己踩过的和见过的坑都列出来:
坑一:忘记join导致僵尸线程
如果你创建了线程,既不join也不detach,线程结束后资源不会被回收。这就跟进程里的僵尸进程一样,会浪费系统资源。我曾经在一个服务程序里忘了join,跑了三天后系统报"无法创建新线程",排查了半天才发现是线程资源没释放。
坑二:传递局部变量给线程
看这段代码:
void create_thread() {
int local_var = 42;
pthread_create(&tid, NULL, worker, &local_var);
// 函数返回后,local_var就失效了!
}
线程可能还没开始执行,local_var就已经被销毁了。正确的做法是:要么用全局变量,要么用malloc分配堆内存。
坑三:pthread_exit和局部变量
在线程函数里,如果你在栈上定义了一个局部变量,然后通过pthread_exit返回它的地址——嗯,这跟坑二是一个道理。线程退出后,栈空间就释放了,外面拿到的指针指向的是无效内存。
2.6 实战建议
最后,分享几个我个人的编码习惯:
- 总是检查返回值:pthread函数的返回值一定要检查。别偷懒,哪怕你觉得"肯定不会失败"。
- 线程函数尽量简单:一个线程函数只做一件事。如果逻辑复杂,拆成多个小函数。
- 明确线程的生命周期:创建线程之前就想清楚:谁来join它?什么时候join?返回值怎么处理?
- 用valgrind检查内存泄漏:多线程程序的内存泄漏特别难查。我每次写完都会跑一遍valgrind,能省不少调试时间。
好了,pthread的三个核心函数就讲到这里。代码不多,但每个细节都值得你亲手敲一遍。下一节咱们会聊线程的属性设置,到时候你会发现——原来线程还能这么玩。