linux系统编程(一):pthread常用函数

Linux pthread 常用函数实战 ------ 从 create 到 TLS

一行 pthread_create 起线程很简单,但要安全地停下、等到它退出、回收资源、还能传数据回来 ------ 一套 pthread API 才够用。这篇把最常用的 14 个函数串起来,每个配最小可运行的 demo。


0. 一个例子

把 1 ~ 10 亿求和,单线程跑约 2 秒。开 4 个线程并行算,每个负责 1/4,理论上能压到 0.5 秒。

复制代码
主线程
  ├─ worker 1:1 ~ 2.5 亿
  ├─ worker 2:2.5 亿 ~ 5 亿
  ├─ worker 3:5 亿 ~ 7.5 亿
  └─ worker 4:7.5 亿 ~ 10 亿
主线程等所有 worker 完成 → 把 4 个结果加起来

这一个场景就用到:

  • pthread_create 起线程
  • pthread_join 等结果
  • pthread_exit / return 带返回值
  • pthread_self 调试时区分线程

下面一个一个看。


1. 线程生命周期:create / join / detach / exit

1.1 pthread_create ------ 起线程

c 复制代码
int pthread_create(pthread_t *thread,
                   const pthread_attr_t *attr,
                   void *(*start_routine)(void *),
                   void *arg);
参数 含义
thread 输出:新线程的 tid 写到这里
attr 属性(栈大小、是否 detached 等),传 NULL 用默认
start_routine 入口函数,签名固定 void *(void *)
arg 传给入口函数的参数

最小例子:

c 复制代码
#include <pthread.h>
#include <stdio.h>

void *worker(void *arg) {
    int id = *(int *)arg;
    printf("worker %d running\n", id);
    return NULL;
}

int main(void) {
    pthread_t tid;
    int id = 42;
    pthread_create(&tid, NULL, worker, &id);
    pthread_join(tid, NULL);
    return 0;
}

⚠️ arg 的生命周期 :上面这个例子主线程 pthread_join 阻塞着,所以 id 这个栈变量是活的。如果改成 pthread_detach 不等就 return,id 已经被回收,worker 读到的就是垃圾。

1.2 pthread_exit ------ 主动退出 + 带返回值

c 复制代码
void pthread_exit(void *retval);

retval 会被 pthread_join 拿到。直接 return 等价于 pthread_exit

c 复制代码
void *worker(void *arg) {
    long sum = 0;
    for (int i = 1; i <= 1000000; i++) sum += i;
    return (void *)sum;        // 等价于 pthread_exit((void *)sum)
}

1.3 pthread_join ------ 阻塞等退出 + 拿返回值 + 回收资源

c 复制代码
int pthread_join(pthread_t thread, void **retval);

阻塞当前线程,等 thread 退出,把它的返回值写到 retval

⚠️ 不 join 也不 detach = 线程退出后资源永远不回收("僵尸线程"),是常见的资源泄漏原因。

把开头那个并行求和例子完整写出来:

c 复制代码
#include <pthread.h>
#include <stdio.h>

#define N 4
#define TOTAL 1000000000L

typedef struct {
    long start, end;
} range_t;

void *sum_range(void *arg) {
    range_t *r = arg;
    long s = 0;
    for (long i = r->start; i <= r->end; i++) s += i;
    return (void *)s;
}

int main(void) {
    pthread_t tids[N];
    range_t ranges[N];
    long step = TOTAL / N;
    long total = 0;

    for (int i = 0; i < N; i++) {
        ranges[i].start = i * step + 1;
        ranges[i].end   = (i + 1) * step;
        pthread_create(&tids[i], NULL, sum_range, &ranges[i]);
    }

    for (int i = 0; i < N; i++) {
        void *ret;
        pthread_join(tids[i], &ret);    // 等退出 + 拿返回值
        total += (long)ret;
    }

    printf("sum = %ld\n", total);
    return 0;
}

1.4 pthread_detach ------ "我不打算等了"

c 复制代码
int pthread_detach(pthread_t thread);

线程被分离后:

  • 不需要 join
  • 退出时资源自动回收
  • 不能再 join 了(再 join 会返回错误)

适用场景:fire-and-forget 的后台任务,比如日志写入、心跳上报、网络服务的 per-connection handler。

c 复制代码
void *background_log(void *arg) {
    while (1) {
        write_log_to_disk();
        sleep(1);
    }
    return NULL;
}

int main(void) {
    pthread_t tid;
    pthread_create(&tid, NULL, background_log, NULL);
    pthread_detach(tid);    // 不打算等了

    do_main_work();
    return 0;
}

也可以在创建时直接设 PTHREAD_CREATE_DETACHED 属性:

c 复制代码
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, ...);
pthread_attr_destroy(&attr);

2. 取消机制:cancel / cleanup / state / type

2.1 pthread_cancel ------ 请求其他线程退出

c 复制代码
int pthread_cancel(pthread_t thread);

向目标线程发取消请求。注意:只是请求,不是强制退出。目标线程在到达"取消点"(cancellation point)时才真的退出。

常见的取消点:read / write / poll / sleep / pthread_cond_wait / pthread_join 等阻塞 syscall。完整列表见 man 7 pthreads

例子:可中断的下载

c 复制代码
void *download_worker(void *arg) {
    while (more_data) {
        int n = recv(sock, buf, sizeof(buf), 0);   // 取消点
        write(file, buf, n);                        // 取消点
    }
    return NULL;
}

int main(void) {
    pthread_t tid;
    pthread_create(&tid, NULL, download_worker, NULL);

    sleep(5);
    pthread_cancel(tid);          // 5 秒后取消下载
    pthread_join(tid, NULL);
    return 0;
}

2.2 pthread_cleanup_push / pop ------ cancel 安全的资源释放

线程在阻塞点被 cancel 时,已经持有的资源(mutex、内存、文件、socket 等)需要自动释放。用 cleanup handler:

c 复制代码
void cleanup_unlock(void *arg) {
    pthread_mutex_unlock((pthread_mutex_t *)arg);
}

void *worker(void *arg) {
    pthread_mutex_lock(&m);
    pthread_cleanup_push(cleanup_unlock, &m);   // 注册 handler

    pthread_cond_wait(&cv, &m);    // 取消点;如果被 cancel,handler 会被自动调

    pthread_cleanup_pop(1);        // 1 = 执行 handler;0 = 仅注销
    return NULL;
}

⚠️ cleanup_push 和 cleanup_pop 必须配对在同一个作用域,因为它们底层是宏,依赖局部变量做记账。

2.3 pthread_setcancelstate / setcanceltype ------ 控制何时响应

c 复制代码
int pthread_setcancelstate(int state, int *oldstate);
//   PTHREAD_CANCEL_ENABLE   - 默认
//   PTHREAD_CANCEL_DISABLE  - 屏蔽 cancel(请求被挂起,state 改回 ENABLE 后才生效)

int pthread_setcanceltype(int type, int *oldtype);
//   PTHREAD_CANCEL_DEFERRED      - 默认,到 cancellation point 才取消
//   PTHREAD_CANCEL_ASYNCHRONOUS  - 立即取消(很危险,几乎不用)

进入临界区前可以暂时禁用 cancel:

c 复制代码
int oldstate;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
do_critical_thing();
pthread_setcancelstate(oldstate, NULL);

2.4 pthread_testcancel ------ 显式检查

如果一段代码完全没有 cancellation point(比如纯 CPU 循环),又想响应 cancel,手动插入:

c 复制代码
for (int i = 0; i < 1000000; i++) {
    do_calc(i);
    if (i % 1000 == 0)
        pthread_testcancel();   // 显式检查
}

3. 线程标识:self / setname_np

3.1 pthread_self ------ 拿自己的 tid

c 复制代码
pthread_t pthread_self(void);

调试 log 常用:

c 复制代码
printf("[tid=%lu] processing\n", (unsigned long)pthread_self());

⚠️ pthread_t 在 glibc 上是 unsigned long,但 POSIX 标准没规定具体类型。跨平台代码要用 pthread_equal(t1, t2) 比较,不要直接用 ==

3.2 pthread_setname_np ------ 设线程名(调试用)

c 复制代码
int pthread_setname_np(pthread_t thread, const char *name);

线程名最长 16 字节(含 \0)。设了之后 top -Hps -eLhtopgdb info threads 都能看到,调试很方便:

c 复制代码
void *worker(void *arg) {
    pthread_setname_np(pthread_self(), "downloader");
    ...
}

_np 后缀是 "non-portable",但 Linux / BSD / macOS 都支持(macOS 上 pthread_setname_np 只接一个参数)。


4. 一次性初始化:pthread_once

c 复制代码
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

保证 init_routine 在多线程环境下只被执行一次,且其他线程会等第一次执行完。

经典场景:线程安全的单例 / 懒初始化。

c 复制代码
#include <pthread.h>

static pthread_once_t once = PTHREAD_ONCE_INIT;
static config_t *g_config = NULL;

static void init_config(void) {
    g_config = load_config_from_file();
}

config_t *get_config(void) {
    pthread_once(&once, init_config);
    return g_config;
}

不管多少个线程同时调 get_configinit_config 只会执行一次,其他线程被阻塞到第一次执行完。

比"双重检查锁定"(DCLP)写法更简洁,且不会出错。


5. 线程局部存储(TLS):key_create / setspecific / getspecific

c 复制代码
int pthread_key_create(pthread_key_t *key, void (*destructor)(void *));
int pthread_setspecific(pthread_key_t key, const void *value);
void *pthread_getspecific(pthread_key_t key);

每个线程拥有自己独立的"key 对应的值"。经典例子:errno 就是 TLS 实现的(每个线程的 errno 互不影响)。

c 复制代码
static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t my_key;

static void destructor(void *value) {
    free(value);
}

static void init_key(void) {
    pthread_key_create(&my_key, destructor);
}

char *get_thread_buffer(void) {
    pthread_once(&once, init_key);

    char *buf = pthread_getspecific(my_key);
    if (!buf) {
        buf = malloc(1024);
        pthread_setspecific(my_key, buf);
    }
    return buf;
}

线程退出时 destructor 自动被调用,释放各自的 buf。

C11 还提供 _Thread_local 关键字(gcc 也支持 __thread),更简洁:

c 复制代码
__thread char buf[1024];   // 每个线程独立一份

只是 __thread 不能动态分配 + 自动释放,复杂场景还是用 pthread_key_*


6. 线程生命周期一图流

线程从 pthread_create 进入 Running 状态后,有三条退出路径:

  1. 正常退出returnpthread_exit → Terminated
  2. 被取消pthread_cancel 触发 + 到达 cancellation point → 执行 cleanup handlers → Terminated
  3. 进程退出:所有线程一起死

Terminated 之后两条回收路径:

  • 被 join:joined 的线程有人收尸,资源回收
  • detached:不需要 join,资源自动回收

7. cancel 的完整流程

pthread_cancel(tid)` 调用后:

  1. 内核给目标线程标记一个 "pending cancel"
  2. 目标线程在到达 cancellation point 时检查标记
  3. 如果 cancelstate 是 ENABLE 且 type 是 DEFERRED → 触发取消
  4. 按 LIFO 倒序执行所有 pthread_cleanup_push 注册的 handler
  5. 调用所有 TLS destructor
  6. 线程退出(相当于 pthread_exit(PTHREAD_CANCELED))

这条链路上任何一步崩了(比如 cleanup handler 自己抛异常),结果是 UB。所以 cleanup handler 必须简单、不阻塞、不抛错


8. 总结表

函数 用途 必须配对/注意
pthread_create 创建线程 之后必须 join 或 detach
pthread_join 等退出 + 拿返回值 + 回收 跟 create 配对
pthread_detach 不等,自动回收 跟 create 配对(二选一)
pthread_exit 主动退出 + 带返回值 -
pthread_self 拿自己 tid 比较用 pthread_equal
pthread_setname_np 设线程名(调试) 名字 ≤ 16 字节
pthread_cancel 请求取消 配 cleanup handler
pthread_cleanup_push/pop 资源清理 handler 必须同作用域
pthread_setcancelstate 启停 cancel enable/disable
pthread_setcanceltype deferred/async async 危险,别用
pthread_testcancel 显式检查 cancel 纯 CPU 循环里用
pthread_once 一次性初始化 配 PTHREAD_ONCE_INIT
pthread_key_create 创建 TLS key 注册 destructor
pthread_setspecific / getspecific 写/读 TLS 配 once 初始化 key

9. 最容易踩的 6 个坑

  1. arg 生命周期 :传栈变量给 detached 线程,主线程出栈后 worker 读到垃圾。要么 malloc,要么保证主线程比子线程活久。

  2. 不 join 也不 detach:线程退出后资源永远不回收,俗称"僵尸线程"。

  3. double join:同一个 tid join 两次是 UB。join 完 tid 就失效了。

  4. cancel 后没 cleanup :mutex / 内存 / 文件描述符泄漏。有 cancel 一定要有 cleanup_push

  5. cleanup_push/pop 不在同一作用域:编译报错(底层是宏 + 局部变量)。

  6. pthread_t 类型假设 :不要假设它是 int 或 unsigned long,跨平台用 pthread_equal 比较。


10. 收尾

线程的核心 API 不多,难的是配对纪律

  • createjoindetach
  • lockunlock(会在第二篇细讲)
  • cancelcleanup_push
  • key_createdestructor

任何一对配偏了,都是潜在的资源泄漏或死锁。

把这 14 个函数掌握,普通工程的多线程需求 90% 能搞定。剩下 10%(高性能调度、特殊信号处理、跨进程同步)才需要进一步学 attr 细节、信号、共享内存、futex 等。