2. POSIX线程库(pthreads)基础

好,咱们正式开始玩多线程了。在Linux世界里,多线程编程几乎绕不开一个东西——pthreads。我当年刚接触它的时候,觉得这名字挺唬人,什么POSIX线程库,听起来像什么高深标准。其实说白了,它就是一套C语言的线程操作接口,你把它当成一把瑞士军刀就行。

2.1 pthreads简介

pthreads,全称POSIX Threads,是IEEE制定的一套线程标准接口。为什么它这么重要?因为几乎所有类Unix系统都支持它——Linux、macOS、FreeBSD,甚至某些嵌入式系统。你写一份pthreads代码,基本可以跨平台编译运行。

我个人习惯把pthreads看作「线程操作的API集合」。它提供了什么?创建线程、销毁线程、线程同步(互斥锁、条件变量)、线程私有数据等等。嗯,这里要注意:pthreads不是语言的一部分,它是库。所以你得告诉编译器去链接它。

核心概念:pthreads中的线程是轻量级进程。它们共享同一进程的地址空间,但各自拥有独立的栈和寄存器上下文。这意味着——线程间通信非常高效,但也容易踩坑(后面会讲)。

2.2 编译链接方法(-lpthread)

写pthreads代码,第一步不是写代码,而是搞清楚怎么编译。我见过不少新手,代码写得没问题,编译死活过不去,最后发现是忘了加链接选项。

编译命令长这样:

gcc -o myprogram myprogram.c -lpthread

关键就在这个 -lpthread。它告诉链接器去链接libpthread.so这个动态库。如果你用CMake,可以这样写:

target_link_libraries(myprogram pthread)

注意:有些老系统上,你可能需要写成 -pthread(带一个横杠)。这不仅是链接选项,还会影响编译器的预处理宏。我建议你优先用 -lpthread,如果不行再试 -pthread

我曾经在一个嵌入式项目里,交叉编译时忘了加这个选项,结果链接阶段报了一堆「undefined reference to pthread_create」。排查了半天,最后发现就是少了个链接参数。从那以后,我写Makefile第一件事就是把-lpthread写上。

2.3 创建第一个线程(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表示使用默认属性
  • start_routine:线程入口函数,格式必须是 void* func(void*)
  • arg:传给入口函数的参数

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

来看一个完整的例子:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* thread_func(void* arg) {
    int id = *(int*)arg;
    printf("线程 %d 正在运行...\n", id);
    return NULL;
}

int main() {
    pthread_t tid;
    int thread_id = 1;

    int ret = pthread_create(&tid, NULL, thread_func, &thread_id);
    if (ret != 0) {
        fprintf(stderr, "创建线程失败,错误码: %d\n", ret);
        return 1;
    }

    printf("主线程继续执行...\n");
    sleep(1);  // 简单等待子线程完成
    return 0;
}

你想想看,这段代码里发生了什么?主线程调用pthread_create,系统会创建一个新线程,从thread_func函数开始执行。主线程自己则继续往下走。两个线程是并发运行的,谁先执行完不一定。

小技巧:线程入口函数的参数是void*类型,你可以传任何东西——整数、结构体指针、甚至NULL。但要注意,传局部变量地址时要小心生命周期。我习惯动态分配内存传进去,在线程里用完再释放。

2.4 等待线程结束(pthread_join)

上面那个例子有个问题——主线程用sleep等待子线程,这太粗糙了。如果子线程执行时间超过1秒呢?或者子线程早就结束了,主线程还在傻等?

正确的做法是用pthread_join。它会让调用线程阻塞,直到目标线程退出。函数原型:

int pthread_join(pthread_t thread, void **retval);
  • thread:要等待的线程ID
  • retval:输出参数,接收线程的返回值(如果不需要,传NULL)

改一下上面的例子:

#include <stdio.h>
#include <pthread.h>

void* thread_func(void* arg) {
    int id = *(int*)arg;
    printf("线程 %d 正在运行...\n", id);
    return (void*)42;  // 返回一个值
}

int main() {
    pthread_t tid;
    int thread_id = 1;
    void* retval;

    pthread_create(&tid, NULL, thread_func, &thread_id);
    pthread_join(tid, &retval);  // 等待线程结束

    printf("线程已退出,返回值: %ld\n", (long)retval);
    return 0;
}

这里有个细节:pthread_join会回收线程资源。如果你不调用它,线程结束后会变成僵尸线程,占用系统资源。我见过一个服务程序,创建了大量线程却不join,最后把系统线程数耗尽了。

避坑指南:我曾经在一个网络代理项目里,主线程创建了工作线程后直接退出了。结果工作线程还在跑,但进程已经结束了——所有线程都被强制终止。正确的做法是:主线程必须等待所有子线程结束,或者使用pthread_detach让线程自动清理。

知识体系总览

下面这张图,把本章的核心逻辑串起来了:

pthreads 基础知识体系 pthreads 线程库 编译链接 -lpthread pthread_create pthread_join gcc -o prog prog.c -lpthread CMake: target_link_libraries 参数: tid, attr, func, arg 入口函数: void* func(void*) 阻塞等待线程退出 回收线程资源 核心:创建 → 执行 → 等待 → 回收

这张图把本章的四个知识点串成了流水线:先搞定编译链接,然后创建线程,线程执行任务,最后主线程等待并回收。每一步都缺一不可。

本章小结:pthreads是Linux多线程编程的基石。你只需要记住三个函数——pthread_create、pthread_join,以及编译时加-lpthread。剩下的,都是在这些基础上搭积木。下一章我们会深入线程的生命周期管理,包括分离线程、取消线程等高级话题。


公众号:蓝海资料掘金营,微信deep3321