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

线程退出有两种方式:

  1. 在线程函数里执行return语句。
  2. 调用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(阻塞等待) 获取返回值 void *retval 创建 执行 退出 等待/获取返回值

流程其实很简单:

  1. 主线程调用pthread_create,子线程诞生。
  2. 子线程执行自己的任务,主线程可以继续干别的事。
  3. 子线程通过return或pthread_exit退出。
  4. 主线程调用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的三个核心函数就讲到这里。代码不多,但每个细节都值得你亲手敲一遍。下一节咱们会聊线程的属性设置,到时候你会发现——原来线程还能这么玩。


可可精品资料库