Linux多线程编程:线程取消与互斥锁的工程实践 写过 Linux 多线程程序的人十有八九遇到过线程退不干净的问题剩下的那一个多半被线上偶发的死锁折磨过。今天这篇是“Linux 应用篇”的第九篇核心只聊两件事线程取消和线程互斥。这两个主题单独看都不算难但放到实际工程里它们几乎总是纠缠在一起——你要安全地终结一个工作线程就必然要处理它手里那把锁你想用互斥锁保护共享数据也得想清楚持锁期间线程被取消会发生什么。这篇文章适合两类读者一类是刚开始写 pthread 多线程、习惯用全局退出标志位来停线程的初学者另一类是已经写了几年服务端或嵌入式程序遇到线程退出后资源泄漏、程序偶发卡死希望系统理解取消点与锁之间关系的工程师。文中不会堆砌理论而是从 API 行为、取消点机制、死锁场景逐步展开最后给一个完整的 C 代码示例拿回去就能编译运行。1. 为什么线程取消与互斥总是一起出问题1.1 线程取消像协调好退出的过程而不是强杀很多初学者第一次接触 pthread_cancel 时都会把它理解成“杀掉线程”。这个理解非常危险。pthread_cancel 做的事情只是向目标线程发送一个取消请求真正决定“什么时候退、退之前做什么”的是在目标线程自己的代码路径上尤其是在取消点cancellation point处才生效。默认情况下线程使用延迟取消模式也就是说取消请求发出去之后线程要等执行到某个取消点函数时才会响应并退出。这就带来一个工程上的常识取消不是一个瞬时动作而是一个需要配合的协议。你不能指望线程在任何一条代码指令处停下来也不能指望操作系统帮你释放堆内存、解锁互斥锁。线程被取消后它会执行通过 pthread_cleanup_push 注册的清理函数然后线程终止资源被运行时回收一部分但程序员自己管理的资源比如 malloc 出来的内存、打开的文件描述符、持有的互斥锁都需要在清理函数里主动处理。如果漏了就是典型的“程序不崩但一直泄漏”的慢性病。另外被取消线程的返回值也有讲究。线程正常 return 时返回值可以通过 pthread_join 拿回来被取消的线程join 拿到的返回值是 PTHREAD_CANCELED。这个值本质上是(void *)-1和业务返回值区分开很容易但前提是你真的去拿返回值了。我见过不少代码只调 pthread_join 不接收返回值等于把“线程到底怎么结束的”这个关键信息直接丢弃了。1.2 互斥锁保护的是共享数据也带来约束线程互斥是另一条主线。pthread_mutex_lock 和 pthread_mutex_unlock 这对接口作用不是“把代码锁住”而是保证同一时间只有一个线程能进入临界区进而保护临界区里操作的共享变量。这个逻辑本身很好理解真正让初学者犯晕的是互斥锁带来的隐含约束。第一个约束是所有权。互斥锁有所有权概念哪个线程调用了 lock 成功就只有这个线程能调用 unlock。换成别的线程去 unlock标准里是未定义行为错误检查类型的锁会返回 EPERM普通锁甚至可能直接搞乱内部状态。所以“谁加锁谁解锁”不是风格问题是正确性问题。第二个约束是成本。加锁和解锁并不是免费的。在 Linux 的 NPTL 实现里无竞争场景下 lock 和 unlock 主要靠 CPU 原子指令完成成本相对低一旦发生竞争lock 可能触发 futex 系统调用让线程在内核里挂起等待解锁时再唤醒它。这一睡一醒中间就是完整的上下文切换开销。所以锁的粒度越小越好争用越少越好这也是后面设计临界区时要时刻记在心里的原则。第三个约束容易被忽略互斥锁同时也是内存屏障。线程 A 在临界区里改了一堆共享变量解锁后线程 B 再拿锁进来读B 一定能看到 A 的修改。这不是运气而是互斥锁的语义保证。千万不能为了省事用 volatile 或者裸变量去替代互斥锁那不是“优化”是给自己埋雷。1.3 两者碰撞的三种典型现场把取消和互斥放在一起讲是因为它们在真实项目里一定会碰撞。我总结了三种最常见的“事故现场”第一种线程持锁期间被取消。比如临界区里调用了 write、read、usleep 这类带取消点的函数取消请求一到线程可能在临界区中间退出。此时锁还捏在它手里其他线程全部卡死。如果没有清理处理程序去解锁整个程序基本就挂住了。第二种线程阻塞在 lock 上时被取消。POSIX 把 pthread_mutex_lock 也列为可能取消点线程在等锁的过程中收到取消请求可能不会进入临界区而是直接进入取消流程。这个行为取决于实现和取消类型但有一点是确定的线程并没有持有锁如果你在清理函数里不加判断地无条件 unlock反而可能破坏锁状态。第三种取消和锁交替出现导致清理逻辑混乱。比如一个线程先拿锁 A再尝试拿锁 B期间又调用了可取消函数取消发生后只释放了 AB 根本还没拿到但清理代码可能误以为 B 也持有并去解锁。这种问题比较隐蔽往往是偶发复现排查起来很折磨人。正因为有这些碰撞正确的姿势才必须是先设计好锁的获取顺序再想清楚取消点在代码路径的哪个位置最后用清理处理程序把异常退出和正常退出统一收口。2. 线程取消三件套pthread_cancel、取消状态与取消点2.1 pthread_cancel 是“请退出”不是“立刻消失”pthread_cancel 的接口声明很简单#include pthread.h int pthread_cancel(pthread_t thread);它向指定线程发送取消请求返回 0 只代表请求发送成功不代表线程已经退出。在线程库里这个请求通常是通过信号机制实现的目标线程的运行时会拦截并标记取消标志真正执行退出动作要等到合适时机。调用 pthread_cancel 之后几乎总是要配合 pthread_join 使用。原因很简单只是发送取消请求线程可能还处于未完全退出的状态你需要 join 来回收它的资源同时也只有 join 能确认线程已经结束。如果不 join 就让主程序继续跑轻则资源泄漏重则线程还拿着共享资源程序下一步操作直接踩雷。还有几个细节值得注意。第一对已经结束的线程调用 pthread_cancel 通常会返回 ESRCH所以不要依赖 cancel 来“清场”。第二如果目标线程把取消状态设成了 PTHREAD_CANCEL_DISABLE取消请求会保留但一直不会执行等它重新启用取消时取消请求会继续生效。第三线程被取消后返回值是 PTHREAD_CANCELED也就是(void *)-1可以通过 join 拿到。2.2 取消状态与取消类型两个容易被忽略的开关响应取消之前线程有两个开关可以控制行为取消状态cancel state和取消类型cancel type。状态开关控制的是“要不要响应取消”类型开关控制的是“什么时候响应取消”。接口取值行为说明pthread_setcancelstate(state, oldstate)PTHREAD_CANCEL_ENABLE默认值允许线程响应取消请求pthread_setcancelstate(state, oldstate)PTHREAD_CANCEL_DISABLE暂时屏蔽取消取消请求会保留但不处理pthread_setcanceltype(type, oldtype)PTHREAD_CANCEL_DEFERRED默认值只在取消点响应pthread_setcanceltype(type, oldtype)PTHREAD_CANCEL_ASYNCHRONOUS可能在任意指令中响应极其危险这两组开关在工程里最常用的组合是在持锁临界区期间临时禁用取消退出临界区再恢复。比如int oldstate; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, oldstate); pthread_mutex_lock(mutex); /* 更新共享数据 */ pthread_mutex_unlock(mutex); pthread_setcancelstate(oldstate, NULL);这样做的目的是让临界区成为一个“不被取消打断”的原子操作。相比之下异步取消基本上只适合那些完全不持有任何质量、也不操作共享数据的纯计算任务否则线程可能正改到一半数据结构就被取消了后面根本没法收拾。我在项目里很少用异步取消不是因为性能不够而是因为“随时可能被打断”带来的不确定性会让所有锁和资源的边界全部失守。2.3 取消点清单与 pthread_testcancel 的用法既然延迟取消只在取消点才生效那搞清楚哪些函数是取消点就成了设计代码的关键。glibc 里常见的取消点函数包括线程相关pthread_join、pthread_cond_wait、pthread_cond_timedwait、pthread_mutex_lock、pthread_rwlock_rdlock、pthread_rwlock_wrlock 等输入输出read、write、open、close、select、poll、accept、recv、send、fsync 等时间与等待sleep、usleep、nanosleep、wait、waitpid、sem_wait 等这个列表不是固定的不同版本库实现可能有差异但它给了我们一个很实用的结论如果线程正在阻塞等待 I/O或者正卡在某个等待条件上取消请求往往能把线程“唤醒”然后进入退出流程。这也是为什么很多人用全局标志位停线程停不掉一换成 pthread_cancel 就立刻生效的底层原因。如果你的工作线程里没有这些系统调用只是在纯计算循环里打转那线程永远不会响应取消。这时就轮到 pthread_testcancel 上场了。它的作用是在当前位置主动创建一个取消点如果已有挂起的取消请求就在这里触发取消流程。for (int i 0; i BIG_NUM; i) { do_some_compute(); if (i % 1000 0) { pthread_testcancel(); } }有个小技巧不要把 pthread_testcancel 放在临界区里面。如果你想在持锁时检查取消请求应该先把锁解开再调用 testcancel否则取消发生在持有锁的瞬间清理函数里就必须负责解锁。与其给自己挖坑不如把取消点都安排在解锁之后。3. 互斥锁使用中的关键细节初始化、锁类型与死锁3.1 先学会正确地初始化和销毁互斥锁互斥锁的初始化有两种方式。第一种是静态分配时用宏初始化pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;这种方式最简单适合全局互斥锁或文件作用域的锁。第二种是动态初始化适用于需要指定属性、锁在结构体里、或者运行期才决定配置的场景pthread_mutex_t mutex; pthread_mutexattr_t attr; pthread_mutexattr_init(attr); pthread_mutexattr_settype(attr, PTHREAD_MUTEX_ERRORCHECK); pthread_mutex_init(mutex, attr); pthread_mutexattr_destroy(attr);动态初始化时pthread_mutexattr 属性对象里有两个常见选项值得关注。一个是锁类型下面会专门讲另一个是进程共享属性默认是 PTHREAD_PROCESS_PRIVATE表示锁只在当前进程内使用如果多个进程共享同一块内存并且要互斥访问里面的数据结构就把属性设成 PTHREAD_PROCESS_SHARED这样锁就能跨进程生效。销毁锁同样有讲究。pthread_mutex_destroy 在锁处于锁定状态、或者有线程正在等待该锁时调用属于未定义行为正常返回错误。所以可靠的销毁时机是先确认所有使用该锁的线程都已经 join 完毕再调用 destroy。这个顺序错了最直接的后果就是下一次创建新锁时可能复用同一块内存埋下一些非常难查的随机问题。3.2 lock、trylock、timedlock 的差异和适用场景pthread_mutex_lock 是阻塞语义拿不到锁就一直等。这个接口最大的优点是逻辑简单最大的缺点是“没有超时”。如果持锁线程永久卡死那么所有等锁线程也会跟着永久卡死。pthread_mutex_trylock 是非阻塞版本拿不到锁立即返回 EBUSY。它适合用在“有空就做没空就干别的”的场景里比如多线程任务调度中线程尝试抢占任务队列抢不到就去处理自己的缓存任务这不是死锁反而能减少锁竞争。pthread_mutex_timedlock 是折中方案指定一个绝对时间的超时超时返回 ETIMEDOUT。它特别适合在嵌入式或服务端程序里用来做故障保护——就算持锁线程真的出问题等待线程也不会无限期卡死能通过超时走降级路径。需要注意的是timedlock 的第二个参数是const struct timespec *abs_timeout也就是绝对时间不是相对时间。很多人在调用前用clock_gettime(CLOCK_REALTIME, ts)取一次当前时间再叠加超时时长才不会算错。一个经常被忽略的规则是即使调用者就是锁的持有者普通互斥锁也不能重复加锁。PTHREAD_MUTEX_NORMAL 类型的锁由同一线程 lock 两次第一次成功后第二次就会死锁因为第二次拿不到锁又没人能解锁。这个问题如果在调试阶段就发现还好怕的是线上偶发复现。3.3 递归锁与错误检查锁按需使用不要无脑Linux 互斥锁类型一共有四种分别是锁类型重复加锁非法解锁典型用途PTHREAD_MUTEX_NORMAL未定义行为通常死锁未定义行为默认锁性能好PTHREAD_MUTEX_ERRORCHECK返回 EDEADLK返回 EPERM调试期诊断问题PTHREAD_MUTEX_RECURSIVE允许递归加锁返回 EPERM递归调用同一临界区PTHREAD_MUTEX_DEFAULT由实现定义由实现定义默认语义通常等同 NORMAL递归锁允许同一个线程多次加锁每次 unlock 次数要匹配才能彻底释放。它解决的场景是一个函数内部要调用另一个同样需要锁的函数比如递归遍历树结构同时对共享日志区写数据。但递归锁也容易被滥用它往往掩盖了“临界区范围过宽”的设计问题。更干净的方案是拆成两个函数内部不持锁、外部统一加锁或者用引用计数辅助。错误检查锁是调试神器。它让重复加锁和非法解锁从“死锁或未定义行为”变成“返回明确错误码”我在前期联调阶段经常把锁属性设成 ERRORCHECK 来排查锁逻辑问题等验证稳定后再切回 NORMAL 锁保证性能。这个习惯建议新手直接养成。3.4 三种典型死锁姿势和四个规避原则死锁是互斥锁项目里最让人头秃的问题实战中最常见的三种姿势是第一种同一线程重复加锁。如前面所说普通锁会直接把自己锁死。第二种多线程按相反顺序加锁。线程 1 先拿锁 A 再拿锁 B线程 2 先拿锁 B 再拿锁 A。当线程 1 持有 A 等待 B线程 2 持有 B 等待 A 时两个线程就永远卡死了。这是最典型的“锁顺序反转”死锁。第三种在错误路径上忘记解锁。函数里有多分支比如中途中止返回但 return 之前没有 unlock。这种情况严格来说不叫死锁但效果一样锁一直被持有其他线程全部阻塞。要规避死锁我总结过四个原则所有线程对多个锁的获取顺序要全局一致比如统一先 A 后 B这样就不会出现环路等待。尽量降低锁的嵌套层级能用一把锁解决就不要引入第二把锁。在复杂的关键业务里用 pthread_mutex_timedlock 替代无超时的 lock给异常情况一个出路。明确写清楚每个锁保护的对象代码审查时重点看持锁期间有没有调用其他会拿锁的函数。排查死锁时gdb 的thread apply all bt几乎是必用命令它能一次性打印所有线程的调用栈立刻看到哪些线程停在 lock 上、各自动作是什么。再配合源文件里的锁顺序设计基本能定位到根因。4. 一个完整的工程案例日志上报线程的取消与互斥4.1 需求拆解与线程结构设计为了把前面的知识点串起来我设计一个贴近实际的小案例一个日志上报模块有 4 个上报线程它们会不断更新一个共享的成功计数并定期往一个日志缓冲区里写一条简要记录。主线程运行 2 秒后主动取消其中两个线程用来模拟故障请求或超时请求的下发剩余两个线程继续跑完全部任务。需求拆解之后核心设计是这样的共享变量 success_count 用一把 counter_mutex 保护所有线程更新它之前必须先加锁。日志缓冲区区域用另一把 log_mutex 保护持锁期间模拟了一次耗时操作。每个工作线程在自己的上下文结构里保存一块 malloc 出来的缓冲象征需要手工释放的任务资源。在线程会频繁执行的路径上每 1000 次循环调用一次 pthread_testcancel方便取消请求快速生效。每个线程注册 pthread_cleanup_push在正常退出时通过 pop(0) 丢弃清理函数在线程被取消时清理函数负责释放缓冲、解锁所有可能持有的锁。两个锁的设计是有意为之目的是模拟真实项目里“全局计数锁”和“日志缓冲区锁”分开管理的场景这样取消发生时才需要认真考虑锁状态的恢复。4.2 完整代码实现#include stdio.h #include stdlib.h #include pthread.h #include unistd.h #define WORKERS_NUM 4 #define WORK_ITERS 5000000 #define CHECK_INTERVAL 1000 static pthread_mutex_t counter_mutex PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t log_mutex PTHREAD_MUTEX_INITIALIZER; static long long success_count 0; typedef struct { int id; char *task_buf; int holding_log_lock; } worker_ctx; static void worker_cleanup(void *arg) { worker_ctx *ctx (worker_ctx *)arg; printf([cleanup] worker %d begin\n, ctx-id); if (ctx-holding_log_lock) { pthread_mutex_unlock(log_mutex); ctx-holding_log_lock 0; printf([cleanup] worker %d unlock log_mutex\n, ctx-id); } if (ctx-task_buf) { free(ctx-task_buf); ctx-task_buf NULL; printf([cleanup] worker %d release task_buf\n, ctx-id); } printf([cleanup] worker %d done\n, ctx-id); } static void *worker_routine(void *arg) { worker_ctx *ctx (worker_ctx *)arg; ctx-task_buf malloc(128 * 1024); if (!ctx-task_buf) { return (void *)0; } pthread_cleanup_push(worker_cleanup, ctx); for (int i 0; i WORK_ITERS; i) { pthread_mutex_lock(counter_mutex); success_count; pthread_mutex_unlock(counter_mutex); if (ctx-id % 2 0) { pthread_mutex_lock(log_mutex); ctx-holding_log_lock 1; usleep(1); ctx-holding_log_lock 0; pthread_mutex_unlock(log_mutex); } if (i % CHECK_INTERVAL 0) { pthread_testcancel(); } } pthread_cleanup_pop(0); printf([worker] %d normal exit\n, ctx-id); return (void *)0; } int main(void) { pthread_t tids[WORKERS_NUM]; worker_ctx ctxs[WORKERS_NUM] {0}; for (int i 0; i WORKERS_NUM; i) { ctxs[i].id i; pthread_create(tids[i], NULL, worker_routine, ctxs[i]); } sleep(2); printf([main] cancel worker 1 and worker 3\n); pthread_cancel(tids[1]); pthread_cancel(tids[3]); for (int i 0; i WORKERS_NUM; i) { void *ret NULL; pthread_join(tids[i], ret); printf([main] join worker %d ret%s\n, i, ret PTHREAD_CANCELED ? PTHREAD_CANCELED : normal); } pthread_mutex_destroy(counter_mutex); pthread_mutex_destroy(log_mutex); printf([main] final success_count%lld\n, success_count); return 0; }这个代码有几个细节要解释一下。首先ctx-holding_log_lock 的作用是标记当前线程是否真的持有了 log_mutex。因为清理函数可能在两种状态下被调用一种是在 log_mutex 临界区里被取消另一种是在临界区外被取消。无条件 unlock 是危险的用标志位判断后再解锁才安全。其次pthread_cleanup_push和pthread_cleanup_pop在 glibc 中的实现会把两个宏之间的代码包在一个代码块里所以它们必须出现在同一个函数、同一个作用域内不能把 push 放在函数头、pop 放在另一个函数里。这种写法看起来有点别扭但这是 API 的现实约束记住就好了。4.3 运行结果与取消点分析在我的实验环境里编译命令是gcc -o thread_demo thread_demo.c -pthread运行结果大致如下具体输出顺序会因调度而不同[worker] 0 normal exit [worker] 1 begin waiting... [main] cancel worker 1 and worker 3 [cleanup] worker 1 begin [cleanup] worker 1 release task_buf [cleanup] worker 1 done [cleanup] worker 3 begin [cleanup] worker 3 release task_buf [cleanup] worker 3 done [main] join worker 0 retnormal [main] join worker 1 retPTHREAD_CANCELED [main] join worker 3 retPTHREAD_CANCELED [main] final success_count小于理论上限这个结果非常关键的地方在于worker 1 和 worker 3 被取消时可能正卡在 pthread_mutex_lock 等待 counter_mutex也可能正卡在 usleep 里等待 log_mutex 被释放但不管哪种情况线程都会被取消请求唤醒然后走入清理函数。清理函数释放了手工分配的 task_buf也安全地判断并解锁了 log_mutex。最终主线程 join 时拿到返回值是 PTHREAD_CANCELED说明它们是通过取消结束的而不是正常跑完循环退出的。另外要注意线程被取消时如果它正阻塞在 counter_mutex 上说明它并没有获得这把锁所以清理函数里不需要对 counter_mutex 做 unlock。设计清理函数时必须区分“我真正持有的锁”和“我可能在等待的锁”这正是代码里 holding_log_lock 标志存在的原因。如果无脑把两把锁都在 cleanup 里解锁反而会引入新的并发错误。4.4 为临界区加上取消保护的两种方案除了清理函数保护临界区还有另一招在临界区前后禁用取消。刚才代码里对 log_mutex 的保护方式其实是“用清理函数兜底”因此即使取消发生在临界区中间清理函数也会负责解锁。另一种更主动的方案是把取消屏蔽打开int oldstate; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, oldstate); pthread_mutex_lock(log_mutex); /* 临界区操作 */ pthread_mutex_unlock(log_mutex); pthread_setcancelstate(oldstate, NULL);两种方式各有适用场景。清理函数适合把“资源释放、解锁”集中处理适合跨多步资源的场景取消屏蔽则更适合一个短小、不需要额外清理逻辑的临界区。我个人的偏好是临界区很短且没有外部资源依赖时用取消屏蔽临界区里涉及多级资源、动态内存、文件句柄时用清理函数统一收口。实际项目中两种方式也经常混合用。核心原则是无论线程是正常退出还是被取消退出它持有过的锁必须释放它申请过的资源必须归还。只要这个原则不被破坏取消和互斥的组合就不会变成事故。5. 实测中常踩的坑与排查技巧5.1 常见问题速查表表面现象根因解决方案调用 pthread_cancel 后线程不退出目标线程取消类型是延迟取消且没有走到任何取消点在循环路径里主动调 pthread_testcancel或改用异步取消类型线程退出后程序卡死被取消线程持锁期间退出锁没有被释放在 pthread_cleanup_push 清理函数里按需解锁join 拿到的返回值不是 PTHREAD_CANCELED线程其实正常跑完并 return或者返回了业务值确认逻辑分支用返回值类型区分结束原因清理函数里无条件解锁导致锁状态错乱线程被取消时根本没有持有该锁unlock 是非法操作用状态标志位记录是否持锁只在持锁时解锁死锁偶发只在高峰并发时出现多线程按不同顺序获取多把锁统一锁获取顺序配合 timedlock 超时保护锁可能在初始化前被使用多线程启动顺序不可控先创建锁再创建线程或把锁初始化放 main 最前面销毁锁时程序崩溃销毁时锁处于被持有状态或有线程在等待先 join 全部相关线程再销毁锁临界区代码运行结果不对编译器或 CPU 对内存访问进行了重排用互斥锁保证临界区数据的可见性不要裸变量共享这张表里的问题和方案基本覆盖了我这些年遇到的绝大多数线程取消与互斥相关故障。排查时还有个习惯值得养成一旦疑似死锁立刻用 gdb 给进程发中断然后执行thread apply all bt看看所有线程都停在哪个函数上。通常一眼就能看出是“等锁”还是“等条件”再回到代码里检查锁的获取顺序和取消点位置问题就清晰了。5.2 我在多线程项目里总结的几条经验第一个经验是不要总是想着用全局标志位代替线程取消。全局退出标志的优点是实现直白但缺点很多尤其是线程阻塞在 I/O 调用上时根本不响应。与其在退出逻辑上不断打补丁不如在规划设计阶段就把取消点和清理函数写清楚把“线程如何优雅退出”作为和“线程如何工作”同等重要的问题对待。第二个经验是取消点要集中管理不要散落在代码各个角落。我比较喜欢的写法是把工作线程的循环体设计成“获取任务—检查取消—执行任务—清理现场”的模式把 pthread_testcancel 放在循环体的顶部或底部。这样看代码的人只需要盯住这一处就能确认取消请求是否会被及时处理而不是在全文件里到处找取消点。第三个经验是锁的粒度和临界区的安全性永远是权衡出来的但取消安全是底线。上线前夕如果有人提议“为了性能把临界区变大并在里面显式调用异步取消”我一般会非常警惕。异步取消对锁的破坏几乎是毁灭性的性能问题可以通过拆分锁、减少锁竞争来解决但一旦出现持锁取消且没有兜底程序连稳定运行都谈不上性能再好也没有意义。第四个经验也是最想分享给新人的一条写第一个多线程版本时就把错误检查锁开起来。加锁之后找个人帮你 code review 锁顺序重点看嵌套锁的不对称路径。这一步治不了所有并发问题但能治好很大一部分尤其适合刚接触 pthread 的开发者。等代码跑稳了再切回普通锁把最后那点性能抠出来也不迟。这四件事基本就是我在多线程项目里踩了足够多坑后留下的条件反射。每次写线程取消相关代码我都会先问自己一句这个线程在哪个位置可能停住停住之后谁负责解锁谁负责释放内存。答案只要清晰代码一般就不会出大问题。