:pthread常用函数)
Linux pthread 常用函数实战 —— 从 create 到 TLS一行pthread_create起线程很简单但要安全地停下、等到它退出、回收资源、还能传数据回来—— 一套 pthread API 才够用。这篇把最常用的 14 个函数串起来每个配最小可运行的 demo。0. 一个例子把 1 ~ 10 亿求和单线程跑约 2 秒。开 4 个线程并行算每个负责 1/4理论上能压到 0.5 秒。主线程 ├─ worker 11 ~ 2.5 亿 ├─ worker 22.5 亿 ~ 5 亿 ├─ worker 35 亿 ~ 7.5 亿 └─ worker 47.5 亿 ~ 10 亿 主线程等所有 worker 完成 → 把 4 个结果加起来这一个场景就用到pthread_create起线程pthread_join等结果pthread_exit/ return 带返回值pthread_self调试时区分线程下面一个一个看。1. 线程生命周期create / join / detach / exit1.1 pthread_create —— 起线程intpthread_create(pthread_t*thread,constpthread_attr_t*attr,void*(*start_routine)(void*),void*arg);参数含义thread输出新线程的 tid 写到这里attr属性栈大小、是否 detached 等传 NULL 用默认start_routine入口函数签名固定void *(void *)arg传给入口函数的参数最小例子#includepthread.h#includestdio.hvoid*worker(void*arg){intid*(int*)arg;printf(worker %d running\n,id);returnNULL;}intmain(void){pthread_ttid;intid42;pthread_create(tid,NULL,worker,id);pthread_join(tid,NULL);return0;}⚠️arg的生命周期上面这个例子主线程pthread_join阻塞着所以id这个栈变量是活的。如果改成pthread_detach不等就 returnid已经被回收worker 读到的就是垃圾。1.2 pthread_exit —— 主动退出 带返回值voidpthread_exit(void*retval);retval会被pthread_join拿到。直接return等价于pthread_exitvoid*worker(void*arg){longsum0;for(inti1;i1000000;i)sumi;return(void*)sum;// 等价于 pthread_exit((void *)sum)}1.3 pthread_join —— 阻塞等退出 拿返回值 回收资源intpthread_join(pthread_tthread,void**retval);阻塞当前线程等thread退出把它的返回值写到retval。⚠️不 join 也不 detach 线程退出后资源永远不回收“僵尸线程”是常见的资源泄漏原因。把开头那个并行求和例子完整写出来#includepthread.h#includestdio.h#defineN4#defineTOTAL1000000000Ltypedefstruct{longstart,end;}range_t;void*sum_range(void*arg){range_t*rarg;longs0;for(longir-start;ir-end;i)si;return(void*)s;}intmain(void){pthread_ttids[N];range_tranges[N];longstepTOTAL/N;longtotal0;for(inti0;iN;i){ranges[i].starti*step1;ranges[i].end(i1)*step;pthread_create(tids[i],NULL,sum_range,ranges[i]);}for(inti0;iN;i){void*ret;pthread_join(tids[i],ret);// 等退出 拿返回值total(long)ret;}printf(sum %ld\n,total);return0;}1.4 pthread_detach —— “我不打算等了”intpthread_detach(pthread_tthread);线程被分离后不需要 join退出时资源自动回收不能再 join 了再 join 会返回错误适用场景fire-and-forget 的后台任务比如日志写入、心跳上报、网络服务的 per-connection handler。void*background_log(void*arg){while(1){write_log_to_disk();sleep(1);}returnNULL;}intmain(void){pthread_ttid;pthread_create(tid,NULL,background_log,NULL);pthread_detach(tid);// 不打算等了do_main_work();return0;}也可以在创建时直接设PTHREAD_CREATE_DETACHED属性pthread_attr_tattr;pthread_attr_init(attr);pthread_attr_setdetachstate(attr,PTHREAD_CREATE_DETACHED);pthread_create(tid,attr,...);pthread_attr_destroy(attr);2. 取消机制cancel / cleanup / state / type2.1 pthread_cancel —— 请求其他线程退出intpthread_cancel(pthread_tthread);向目标线程发取消请求。注意只是请求不是强制退出。目标线程在到达取消点cancellation point时才真的退出。常见的取消点read/write/poll/sleep/pthread_cond_wait/pthread_join等阻塞 syscall。完整列表见man 7 pthreads。例子可中断的下载void*download_worker(void*arg){while(more_data){intnrecv(sock,buf,sizeof(buf),0);// 取消点write(file,buf,n);// 取消点}returnNULL;}intmain(void){pthread_ttid;pthread_create(tid,NULL,download_worker,NULL);sleep(5);pthread_cancel(tid);// 5 秒后取消下载pthread_join(tid,NULL);return0;}2.2 pthread_cleanup_push / pop —— cancel 安全的资源释放线程在阻塞点被 cancel 时已经持有的资源mutex、内存、文件、socket 等需要自动释放。用 cleanup handlervoidcleanup_unlock(void*arg){pthread_mutex_unlock((pthread_mutex_t*)arg);}void*worker(void*arg){pthread_mutex_lock(m);pthread_cleanup_push(cleanup_unlock,m);// 注册 handlerpthread_cond_wait(cv,m);// 取消点如果被 cancelhandler 会被自动调pthread_cleanup_pop(1);// 1 执行 handler0 仅注销returnNULL;}⚠️cleanup_push 和 cleanup_pop 必须配对在同一个作用域因为它们底层是宏依赖局部变量做记账。2.3 pthread_setcancelstate / setcanceltype —— 控制何时响应intpthread_setcancelstate(intstate,int*oldstate);// PTHREAD_CANCEL_ENABLE - 默认// PTHREAD_CANCEL_DISABLE - 屏蔽 cancel请求被挂起state 改回 ENABLE 后才生效intpthread_setcanceltype(inttype,int*oldtype);// PTHREAD_CANCEL_DEFERRED - 默认到 cancellation point 才取消// PTHREAD_CANCEL_ASYNCHRONOUS - 立即取消很危险几乎不用进入临界区前可以暂时禁用 cancelintoldstate;pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,oldstate);do_critical_thing();pthread_setcancelstate(oldstate,NULL);2.4 pthread_testcancel —— 显式检查如果一段代码完全没有 cancellation point比如纯 CPU 循环又想响应 cancel手动插入for(inti0;i1000000;i){do_calc(i);if(i%10000)pthread_testcancel();// 显式检查}3. 线程标识self / setname_np3.1 pthread_self —— 拿自己的 tidpthread_tpthread_self(void);调试 log 常用printf([tid%lu] processing\n,(unsignedlong)pthread_self());⚠️pthread_t在 glibc 上是unsigned long但 POSIX 标准没规定具体类型。跨平台代码要用pthread_equal(t1, t2)比较不要直接用。3.2 pthread_setname_np —— 设线程名调试用intpthread_setname_np(pthread_tthread,constchar*name);线程名最长 16 字节含\0。设了之后top -H、ps -eL、htop、gdb info threads都能看到调试很方便void*worker(void*arg){pthread_setname_np(pthread_self(),downloader);...}_np后缀是 “non-portable”但 Linux / BSD / macOS 都支持macOS 上pthread_setname_np只接一个参数。4. 一次性初始化pthread_onceintpthread_once(pthread_once_t*once_control,void(*init_routine)(void));保证init_routine在多线程环境下只被执行一次且其他线程会等第一次执行完。经典场景线程安全的单例 / 懒初始化。#includepthread.hstaticpthread_once_toncePTHREAD_ONCE_INIT;staticconfig_t*g_configNULL;staticvoidinit_config(void){g_configload_config_from_file();}config_t*get_config(void){pthread_once(once,init_config);returng_config;}不管多少个线程同时调get_configinit_config只会执行一次其他线程被阻塞到第一次执行完。比双重检查锁定DCLP写法更简洁且不会出错。5. 线程局部存储TLSkey_create / setspecific / getspecificintpthread_key_create(pthread_key_t*key,void(*destructor)(void*));intpthread_setspecific(pthread_key_tkey,constvoid*value);void*pthread_getspecific(pthread_key_tkey);每个线程拥有自己独立的key 对应的值。经典例子errno就是 TLS 实现的每个线程的 errno 互不影响。staticpthread_once_toncePTHREAD_ONCE_INIT;staticpthread_key_tmy_key;staticvoiddestructor(void*value){free(value);}staticvoidinit_key(void){pthread_key_create(my_key,destructor);}char*get_thread_buffer(void){pthread_once(once,init_key);char*bufpthread_getspecific(my_key);if(!buf){bufmalloc(1024);pthread_setspecific(my_key,buf);}returnbuf;}线程退出时destructor自动被调用释放各自的 buf。C11 还提供_Thread_local关键字gcc 也支持__thread更简洁__threadcharbuf[1024];// 每个线程独立一份只是__thread不能动态分配 自动释放复杂场景还是用pthread_key_*。6. 线程生命周期一图流线程从pthread_create进入 Running 状态后有三条退出路径正常退出return或pthread_exit→ Terminated被取消pthread_cancel触发 到达 cancellation point → 执行 cleanup handlers → Terminated进程退出所有线程一起死Terminated 之后两条回收路径被 joinjoined 的线程有人收尸资源回收detached不需要 join资源自动回收7. cancel 的完整流程pthread_cancel(tid) 调用后内核给目标线程标记一个 “pending cancel”目标线程在到达 cancellation point 时检查标记如果 cancelstate 是 ENABLE 且 type 是 DEFERRED → 触发取消按 LIFO 倒序执行所有pthread_cleanup_push注册的 handler调用所有 TLS destructor线程退出相当于 pthread_exit(PTHREAD_CANCELED)这条链路上任何一步崩了比如 cleanup handler 自己抛异常结果是 UB。所以 cleanup handler 必须简单、不阻塞、不抛错。8. 总结表函数用途必须配对/注意pthread_create创建线程之后必须 join 或 detachpthread_join等退出 拿返回值 回收跟 create 配对pthread_detach不等自动回收跟 create 配对二选一pthread_exit主动退出 带返回值-pthread_self拿自己 tid比较用pthread_equalpthread_setname_np设线程名调试名字 ≤ 16 字节pthread_cancel请求取消配 cleanup handlerpthread_cleanup_push/pop资源清理 handler必须同作用域pthread_setcancelstate启停 cancelenable/disablepthread_setcanceltypedeferred/asyncasync 危险别用pthread_testcancel显式检查 cancel纯 CPU 循环里用pthread_once一次性初始化配 PTHREAD_ONCE_INITpthread_key_create创建 TLS key注册 destructorpthread_setspecific/getspecific写/读 TLS配 once 初始化 key9. 最容易踩的 6 个坑arg 生命周期传栈变量给 detached 线程主线程出栈后 worker 读到垃圾。要么malloc要么保证主线程比子线程活久。不 join 也不 detach线程退出后资源永远不回收俗称僵尸线程。double join同一个 tid join 两次是 UB。join 完 tid 就失效了。cancel 后没 cleanupmutex / 内存 / 文件描述符泄漏。有 cancel 一定要有 cleanup_push。cleanup_push/pop 不在同一作用域编译报错底层是宏 局部变量。pthread_t类型假设不要假设它是 int 或 unsigned long跨平台用pthread_equal比较。10. 收尾线程的核心 API 不多难的是配对纪律create配join或detachlock配unlock会在第二篇细讲cancel配cleanup_pushkey_create配destructor任何一对配偏了都是潜在的资源泄漏或死锁。把这 14 个函数掌握普通工程的多线程需求 90% 能搞定。剩下 10%高性能调度、特殊信号处理、跨进程同步才需要进一步学 attr 细节、信号、共享内存、futex 等。