前言POSIX 线程POSIX threads通常简称 pthreads是类 Unix 系统上的线程 API头文件是pthread.h链接时通常要加-pthread。它不是 C 标准的一部分标准 C 的线程接口在thread、mutex、atomic、condition_variable里。题目说的是 Pthread本文就按 POSIX 接口讲并在最后说明它与std::thread的对应关系。一个典型误解是以为 pthread 提供了类似pthread_get_state()的函数可以随时查询线程处于「运行」「就绪」还是「阻塞」。POSIX 并没有规定这样的查询接口。线程是否在 CPU 上运行、是否在等锁属于调度器内部状态用户态能观察到的只有「可 join」「已 detach」「已取消/已退出」这几件事。另一个常见误解是 pthread 函数失败时会设置errno。POSIX 线程函数通常直接返回错误码成功返回 0不修改errno。把它们当成open、read那样去检查errno会把真正的错误吞掉。本文讲四件事线程的创建与参数传递、join 与 detach 两种回收方式、线程状态的实际含义、属性对象与取消机制。所有示例都只适用于 POSIX 平台Linux、macOS 等在 Windows 上需要别的线程 API。一、创建线程pthread_create 的四个参数函数原型是int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void *),void *arg);四个参数分别是输出参数用来接收新线程的标识线程属性传nullptr表示用默认属性线程入口函数传给入口函数的唯一参数。返回值 0 表示成功否则返回错误码常见的有EAGAIN资源不足、EINVAL属性非法、EPERM权限不足。入口函数的签名固定为void* (void*)只能收一个指针、返回一个指针。要传多个值就把它们打包进一个结构体传结构体的地址// 适用于 POSIX 平台需链接 -pthread#include pthread.h#include stdio.h#include stdlib.hstruct Task {int id;int value;long result;};void* worker(void* raw) {struct Task* t (struct Task*)raw;t-result (long)t-value * 2;printf(task %d done\n, t-id);return NULL;}int main(void) {pthread_t tid;struct Task task {1, 21, 0};if (pthread_create(tid, NULL, worker, task) ! 0) {fprintf(stderr, create failed\n);return 1;}if (pthread_join(tid, NULL) ! 0) {fprintf(stderr, join failed\n);return 1;}printf(result %ld\n, task.result);return 0;}关键点是task的生命周期。它位于main的栈上main在pthread_join返回之前不会退出所以这样传是安全的。如果入口参数指向的是某个函数内的局部变量而该函数先于线程结束返回线程读到的就是悬垂指针属于 UB。二、join 与 detach两种回收方式新建的线程默认是可 joinjoinable的。可 join 的线程结束时它的退出状态和一部分资源要等别的线程调用pthread_join才回收。如果既不 join 也不 detach线程结束后资源会一直挂着类似进程里的僵尸进程。pthread_join做三件事阻塞调用者直到目标线程结束回收目标线程的资源可选地把入口函数的返回值写到输出参数里。它有两个常见错误对同一个线程 join 两次第二次行为未定义、线程自己 join 自己返回EDEADLK。pthread_detach把线程标记为分离状态线程结束时资源自动回收之后不能再 join。适合「发起后不关心结果」的后台任务。// 适用于 POSIX 平台#include pthread.h#include stdio.h#include unistd.hstatic void* background(void* arg) {(void)arg;/* 做一些不需要汇报结果的工作 */return NULL;}int main(void) {pthread_t tid;if (pthread_create(tid, NULL, background, NULL) ! 0) {return 1;}if (pthread_detach(tid) ! 0) {return 1;}/* 不能再 pthread_join(tid)否则行为未定义 */return 0;}pthread_detach也可以在创建时通过属性指定见下一节。注意detach 之后pthread_t仍是一个值但不能用于 join也不保证还能标识任何东西。线程可能已经退出标识符可能被复用。三、线程状态到底指什么POSIX 没有为线程定义一套可查询的状态枚举但可以从两个层面理解。第一层是调度状态由操作系统维护运行running正在某个 CPU 上执行、就绪runnable可以运行但还没被调度、阻塞blocked在等锁、等条件变量、等 I/O 或sleep。这些状态之间的迁移由调度器和同步原语控制用户代码不能用某个 pthread 函数直接读到。第二层是生命周期状态由 pthread 接口体现可 join、已分离、已退出、已取消。下面这张表把常见操作与状态变化对应起来。当前状态操作结果新建pthread_join阻塞到线程结束回收资源变为不可 join新建pthread_detach变为分离结束时自动回收已分离pthread_join失败返回EINVAL已 join再次pthread_join行为未定义运行中pthread_cancel发出取消请求是否响应取决于取消设置已退出但未 join进程退出资源随进程回收但线程栈等不会单独释放想观察线程是否还活着通常用「能否 join」来间接判断而不是查状态。另一个常被忽略的事实是pthread_t在不同平台上可能是整数也可能是指针不要用之外的方式比较也不要用printf(%d)打印比较两个线程标识应当用pthread_equal。四、属性对象与取消pthread_attr_t是一个不透明类型必须先pthread_attr_init用完pthread_attr_destroy。常用设置有两个分离状态和栈大小。// 适用于 POSIX 平台#include pthread.h#include stdio.hstatic void* run(void* arg) {(void)arg;return NULL;}int main(void) {pthread_attr_t attr;pthread_t tid;if (pthread_attr_init(attr) ! 0) {return 1;}pthread_attr_setdetachstate(attr, PTHREAD_CREATE_DETACHED);pthread_attr_setstacksize(attr, 256 * 1024);int rc pthread_create(tid, attr, run, NULL);pthread_attr_destroy(attr); /* 创建之后就可以销毁属性对象 */return rc 0 ? 0 : 1;}栈大小不是随便设的低于PTHREAD_STACK_MIN会失败EINVAL而且线程里的调用深度、局部数组大小都要留余量。默认栈通常有几 MB线程多了会占用大量虚拟地址空间。取消cancellation是 pthread 特有的机制。pthread_cancel只是发出请求目标线程在「取消点」才会响应。默认的取消类型是延迟取消deferredread、write、sleep、pthread_cond_wait等是取消点纯计算循环里不调用这些函数线程就不会被取消。可以用pthread_setcancelstate暂时屏蔽取消用pthread_setcanceltype改为异步取消asynchronous但异步取消可能在任意指令处发生极难保证一致性通常不建议。线程被取消时已经持有的互斥量不会自动释放这正是取消最容易造成死锁的地方。需要在线程里用pthread_cleanup_push/pthread_cleanup_pop注册清理函数在取消发生时解锁、关闭文件。这两个是宏必须成对出现在同一个作用域里。完整示例多线程求和下面把创建、传参、join 和互斥量放在一起。每个线程处理数组的一段最后把局部和累加到共享变量// 适用于 POSIX 平台需链接 -pthread#include pthread.h#include stdio.h#include stdlib.h#define N 1000#define THREADS 4struct Range {const int* data;int begin;int end;long partial;};static pthread_mutex_t g_mutex PTHREAD_MUTEX_INITIALIZER;static long g_total 0;static void* sum_range(void* raw) {struct Range* r (struct Range*)raw;long local 0;for (int i r-begin; i r-end; i) {local r-data[i];}r-partial local;pthread_mutex_lock(g_mutex);g_total local;pthread_mutex_unlock(g_mutex);return NULL;}int main(void) {int data[N];for (int i 0; i N; i) {data[i] i 1;}pthread_t tids[THREADS];struct Range ranges[THREADS];for (int k 0; k THREADS; k) {ranges[k].data data;ranges[k].begin k * (N / THREADS);ranges[k].end (k 1) * (N / THREADS);ranges[k].partial 0;if (pthread_create(tids[k], NULL, sum_range, ranges[k]) ! 0) {fprintf(stderr, create %d failed\n, k);return 1;}}for (int k 0; k THREADS; k) {pthread_join(tids[k], NULL);}printf(total %ld\n, g_total);return 0;}ranges和tids都是main的局部数组join 之前main不会返回所以线程访问它们安全。g_total的读改写放在互斥量里否则就是数据竞争。按标准语义这段程序应当输出 500500但这里没有编译运行环境结论来自求和公式而非执行结果。常见坑点1. 把返回值当 errno 检查❌pthread_create(...); if (errno) { ... }✅int rc pthread_create(...); if (rc ! 0) { /* rc 就是错误码 */ }。2. 把局部变量地址传给线程❌ 在函数里int x 1; pthread_create(t, NULL, f, x);然后函数立刻返回。 ✅ 用堆分配、静态存储或保证调用者活到 join 之后。3. 忘记 join 或 detach❌ 创建线程后既不 join 也不 detach线程结束后资源不回收。 ✅ 二选一把pthread_t包进 RAII 类是个好办法。4. 对同一线程 join 两次❌ 两个线程都去 join 同一个目标。 ✅ 明确唯一的回收者重复 join 是未定义行为。5. 取消持锁的线程❌ 线程拿着pthread_mutex_t时被取消锁永远不释放。 ✅ 用清理函数解锁或用pthread_setcancelstate在临界区内屏蔽取消。6. 用printf打印pthread_t❌printf(%d, tid);✅ 用pthread_equal(a, b)比较需要打印时转成unsigned long并配合实现相关的转换。7. 链接时漏掉-pthread❌ 只写g main.cpp出现未定义符号。 ✅ 编译和链接都加-pthread它同时定义必要的宏。8. 在 C 里混用 pthread 与std::thread的同步对象❌ 用pthread_mutex_lock去锁std::mutex。 ✅ 两套 API 的对象不能互换选一套用到底或只在边界处转换。总结概念pthread 接口C 标准对应创建线程pthread_createstd::thread构造等待结束pthread_joinjoin()分离pthread_detachdetach()线程标识pthread_tpthread_equalstd::thread::id互斥量pthread_mutex_tstd::mutex条件变量pthread_cond_tstd::condition_variable一次性初始化pthread_oncestd::call_oncepthread 的「状态」不是一个可以查询的字段而是调度器行为与生命周期标志的组合写并发代码时真正要关心的是参数活多久、谁来回收、取消发生时锁在谁手里。新项目如果不需要与 C 接口对接优先用标准 C 的线程库可移植性和类型安全都更好。