如何确保销毁互斥锁前无其他线程获取该锁?——对pthread_mutex_destroy手册中代码逻辑的疑问
这个问题确实是多线程资源销毁场景里的经典坑点,咱们先从你贴的代码逻辑入手拆解问题,再逐一解答你的疑问。
首先看这段代码的隐患:
obj_done(struct obj *op) { pthread_mutex_lock(&op->om); if (--op->refcnt == 0) { pthread_mutex_unlock(&op->om); (A) pthread_mutex_destroy(&op->om); (B) free(op); } else (C) pthread_mutex_unlock(&op->om); }
当op->refcnt减到0时,代码先解锁了互斥锁op->om,再去执行销毁和内存释放。这中间存在一个关键的时间窗口:解锁后到销毁mutex前,其他线程完全有可能尝试去获取这个mutex——如果此时有线程还没意识到该对象要被销毁,依然调用pthread_mutex_lock(&op->om),紧接着你就销毁了这个mutex,这会直接触发未定义行为(比如线程崩溃、死锁或者其他难以排查的诡异问题)。
你的两个疑问解答
1. 是否需要使用额外的互斥锁来避免这种情况?
不一定非要额外加互斥锁,但如果用一个全局/层级更高的互斥锁来覆盖该类对象的全生命周期操作(包括创建、引用计数修改、销毁),确实能解决问题。比如所有线程在访问任何该类对象之前,都必须先持有这个全局锁,这样当你在obj_done里发现refcnt归0时,全局锁还在手里,其他线程根本没机会访问这个对象,你可以安全地销毁mutex并释放内存,最后再解锁全局锁。
但这种方式的缺点也很明显:全局锁会成为性能瓶颈,尤其是当这类对象的访问频率很高时。所以大多数场景下,我们会选择更轻量的设计,而非额外加锁。
2. 还是由客户端负责在引用计数归0后不再尝试增加引用计数?
这才是更常见、更合理的设计思路。核心逻辑是:所有访问对象的操作,都必须先保证成功获取到引用计数的增量——换句话说,任何线程想要操作对象的mutex或其他成员,都必须先调用类似obj_acquire的函数,该函数会先检查引用计数,如果已经归0,直接返回失败;如果大于0,再原子地增加引用计数,之后才能进行后续操作。
举个配套的obj_acquire实现例子:
int obj_acquire(struct obj *op) { pthread_mutex_lock(&op->om); if (op->refcnt == 0) { pthread_mutex_unlock(&op->om); return -1; // 对象即将销毁,无法获取引用 } op->refcnt++; pthread_mutex_unlock(&op->om); return 0; }
这样一来,当obj_done里把refcnt减到0后,后续所有obj_acquire都会失败,也就不会有新线程去尝试获取该对象的mutex了。而对于已经在pthread_mutex_lock(&op->om)里阻塞等待的线程,当它拿到锁后,会看到refcnt已经归0,解锁后直接返回失败,不会再操作对象——此时你再销毁mutex,就完全安全了。
当然,这个逻辑成立的前提是:客户端必须严格遵守访问规则,所有对对象的操作都必须通过obj_acquire和obj_done来管理引用计数,不能直接跳过引用检查去操作对象的成员。
内容的提问来源于stack exchange,提问作者Snowball

