当前位置: 首页 > news >正文

进程和线程创建销毁时mutex死锁问题分析

一、问题说明

linux下C语言多线程编程,通常使用mutex互斥锁解决竞争问题,但是在进程和线程创建销毁时,可能导致意外的死锁问题。

  • 进程

linux下父子进程地址空间是独立的,采用写时复制(COW)的原则,使用fork进程创建时,子进程地址空间内容与父进程完全一致,若父进程已持锁,子进程会继承锁的状态,后续子进程再访问此锁时,会出现死锁。

而子进程销毁时,因为与父进程地址空间已完全独立,子进程销毁不会影响父进程的锁的状态。

父进程子进程mutex_lockforkmutex_lock -死锁父进程子进程
  • 线程

linux下线程pthread地址空间,父子线程是共用的,所以创建后子线程可以正常持锁。就算父线程此时已持锁,子线程再持锁,也不会导致死锁,等待父线程释放锁,子线程就可以正常获取锁,mutex本身设计之初就是解决线程之间互斥的。

线程销毁时,由于父子线程地址空间共用,若线程退出时已经持锁,线程使用pthread_cancel退出后,父线程再获取此锁,会导致死锁问题。

线程1线程2mutex_lockpthread_cancelmutex_lock -死锁线程1线程2
  • 汇总
对象创建销毁
进程继承父进程锁状态,可能死锁。无问题
线程无问题子线程可能持锁后退出,可能死锁

本质上,都是由创建销毁新上下文时,锁的状态无法自动释放锁,还是维持之前错误的状态,从设计上其实不难做到,不理解为何有这个历史遗留问题。

二、进程创建时死锁问题

2.1 锁的状态与风险

  • 锁状态复制:如果父进程中某个线程正持有锁,那么在子进程中,该锁会表现为已被持有的状态(即使实际持有它的那个父进程线程并不存在于子进程中)。
  • 未定义行为风险:子进程尝试操作(例如锁定或解锁)这个从父进程继承而来的、状态可能不一致的互斥锁,可能导致未定义行为,常见的是死锁

2.2 底层处理与解决方案

互斥锁的状态管理通常由 Pthreads 库(C 库,如 glibc) 实现,但库的实现会依赖 Linux 内核提供的底层同步机制(如 futex)来保证原子性和阻塞/唤醒操作。

为了避免问题,POSIX 标准提供了 pthread_atfork()函数来帮助安全地处理 fork 与互斥锁的交互:

#include <pthread.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 在fork()之前调用pthread_atfork()来注册处理函数
void pre_fork() { pthread_mutex_lock(&mutex); }
void post_fork_parent() { pthread_mutex_unlock(&mutex); }
void post_fork_child() { // 在子进程中,可能需要将锁重置到未锁定状态,或进行其他初始化// 但直接解锁可能不安全,更常见的做法是避免在子进程使用父进程的锁,或者使用其他同步原语pthread_mutex_unlock(&mutex); // 或者更安全的方法:在子进程中立即调用exec系列函数,不再依赖父进程的状态
}int main() {pthread_atfork(pre_fork, post_fork_parent, post_fork_child);// ... 其他代码,包括fork() ...
}
  • pthread_atfork()注册的三个处理函数:
    • pre_fork:在 fork 之前调用。通常用于锁定所有父进程中的互斥锁,确保 fork 发生时父进程处于一个确定的、稳定的状态。
    • post_fork_parent:在 fork 之后,在父进程中调用。用于释放pre_fork中锁定的所有互斥锁。
    • post_fork_child:在 fork 之后,在子进程中调用。这是最关键的环节。子进程需要谨慎处理继承来的锁状态。有时会选择直接解锁,但更安全和常见的做法是:
      • 立即调用 exec系列函数:如果子进程计划调用 exec来执行一个新程序,那么父进程地址空间(包括那些锁)会被完全替换,这就自动避免了继承锁状态的问题。这是最推荐和最简单的做法。
      • 避免使用继承的锁:如果子进程不调用 exec,则需要非常小心地处理所有从父进程继承的同步原语。有时可能需要子进程完全重新初始化自己的同步环境。

最佳实践:如果子进程在 fork 后立即调用 exec()执行新程序,那么继承的锁状态会被新程序覆盖,无需特殊处理。若不调用 exec(),则需通过 pthread_atfork()等方式确保锁在子进程中的状态安全,或考虑使用其他进程间同步机制(如信号量、文件锁等)。

2.3 典型案例-popen引发的血案

在多线程架构中,使用popen执行shell命令,导致的死锁。某些glibc版本中,popen函数中vfork后可能会访问fd list进行持锁,然后再支持exec,在执行exec就持锁,导致出现死锁问题。

vfork
fd lock
exec

尽量少使用进程和线程混搭的情况。

三、线程销毁时死锁问题

使用 pthread_cancel取消一个正在持有互斥锁的线程是危险的,如果取消请求恰好在线程持有锁时发生,并且该线程在被取消前没有机会释放锁,那么这个锁就会永远处于锁定状态,导致其他等待该锁的线程死锁

3.1 问题根源

线程的取消点(Cancellation Points)是线程检查是否被取消并响应的位置。许多系统调用和库函数(如 pthread_cond_wait, read, write, sleep)都是取消点。如果取消请求发生在线程进入取消点之后但尚未释放锁之前,就可能引发问题。

3.2 解决方案

🔒 使用线程清理处理程序(Cleanup Handlers)

这是最直接和推荐的方法。pthreads 库提供了 pthread_cleanup_push()pthread_cleanup_pop()宏,用于注册和移除清理函数。这些函数在线程被取消或通过 pthread_exit()退出时自动执行,用于释放资源(如互斥锁)。

#include <pthread.h>
#include <stdio.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 清理函数,用于解锁
void cleanup_handler(void *arg) {pthread_mutex_unlock((pthread_mutex_t *)arg);printf("Cleanup handler: mutex unlocked\n");
}void *thread_func(void *arg) {// 将清理处理程序压栈pthread_cleanup_push(cleanup_handler, &mutex); pthread_mutex_lock(&mutex); // 加锁// 临界区操作...printf("Thread is working in critical section...\n");// 假设这里是一个取消点(如某些系统调用),或者循环检查取消请求sleep(5); // sleep是一个取消点// 正常解锁并弹出清理处理程序(参数非0表示执行清理函数,0表示不执行)pthread_mutex_unlock(&mutex);pthread_cleanup_pop(0); // 正常执行到此处,弹出清理函数但不执行它return NULL;
}int main() {pthread_t tid;pthread_create(&tid, NULL, thread_func, NULL);sleep(2); // 让子线程运行并获取锁pthread_cancel(tid); // 发送取消请求pthread_join(tid, NULL); // 等待线程结束printf("Main thread: joined successfully.\n");return 0;
}

在这个例子中,即使 thread_funcsleep(一个取消点)时被取消,cleanup_handler也会被调用并释放互斥锁,从而防止死锁。

🏁 禁用取消或使用延迟取消
  • 禁用取消:在线程进入临界区前,使用 pthread_setcancelstate()临时禁用取消功能。

    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_state);
    pthread_mutex_lock(&mutex);
    // 临界区操作
    pthread_mutex_unlock(&mutex);
    pthread_setcancelstate(old_state, NULL);
    

    这可以确保线程在持有锁时不会被取消,但需谨慎使用,因为可能导致取消请求响应不及时。

  • 使用延迟取消(Deferred Cancellation):这是默认的取消类型。线程只会在到达取消点时才会响应取消请求。你可以确保在临界区内没有取消点(注意:某些函数可能是隐藏的取消点),或者在临界区内使用 pthread_testcancel()手动创建取消点,但这需要精心设计。

🚩 使用标志位安全终止线程(推荐替代方法)

避免使用 pthread_cancel,而是采用协作式取消。在线程函数内周期性地检查一个全局或传入的标志位,当该标志位被设置时,线程主动清理资源并退出。

#include <stdatomic.h>
// 或使用 volatile 和互斥锁保护atomic_bool stop_requested = ATOMIC_VAR_INIT(false); // C11 原子变量
// 或者 volatile bool stop_requested = false; 并结合互斥锁确保可见性void *thread_func(void *arg) {while (!atomic_load(&stop_requested)) { // 检查停止请求pthread_mutex_lock(&mutex);// 临界区工作pthread_mutex_unlock(&mutex);// ... 其他工作}// 线程安全地清理资源后退出return NULL;
}// 在另一个线程中请求该线程停止:
atomic_store(&stop_requested, true);

这种方法完全避免了异步取消带来的不确定性,是最安全、最可控的线程终止方式。

3.3 典型案例

线程退出点通常是一些睡眠的API,使用这些API时如果有加锁则更容易引发死锁问题,比如发包函数等。

线程1线程2mutex_locksend(线程退出点)pthread_cancelmutex_lock -死锁线程1线程2

3.4 参考资料

线程退出点、退出点等介绍参考如下文章。

https://blog.csdn.net/chengf223/article/details/117999110

四、实践建议与总结

场景关键问题推荐解决方案
fork() 与互斥锁子进程继承锁状态可能导致死锁子进程后立即调用 exec();或使用 pthread_atfork()管理锁状态
pthread_cancel 与持锁线程线程被取消时锁未释放导致死锁首选:使用 pthread_cleanup_push/pop注册清理函数释放锁 更安全选择:使用协作式取消(标志位)替代 pthread_cancel


文章转载自:

http://TDUdqqdC.gtwtk.cn
http://ByKdFo2r.gtwtk.cn
http://rMBcfZDB.gtwtk.cn
http://BwgyW1rp.gtwtk.cn
http://HEyRliwm.gtwtk.cn
http://786KPULz.gtwtk.cn
http://JQdkYXmc.gtwtk.cn
http://k7Hfi4Ax.gtwtk.cn
http://jJr1ycuk.gtwtk.cn
http://xXSU0CVO.gtwtk.cn
http://iRooIqFH.gtwtk.cn
http://7Hc3lA6h.gtwtk.cn
http://REx9SXgG.gtwtk.cn
http://Jwce4Sfi.gtwtk.cn
http://iKxp0hmj.gtwtk.cn
http://77882eF9.gtwtk.cn
http://xOusczwm.gtwtk.cn
http://dSrI9YAD.gtwtk.cn
http://VQmp25FI.gtwtk.cn
http://i6sOI1VY.gtwtk.cn
http://zJNbbAzh.gtwtk.cn
http://fmD6sxld.gtwtk.cn
http://mFKbT1Ci.gtwtk.cn
http://NEJGHxAC.gtwtk.cn
http://J5AfzAPn.gtwtk.cn
http://ZN7NQT4y.gtwtk.cn
http://5OZ9rTYN.gtwtk.cn
http://MVyWqCf0.gtwtk.cn
http://gWlhmjdF.gtwtk.cn
http://2XW8Amw3.gtwtk.cn
http://www.dtcms.com/a/369442.html

相关文章:

  • vsan default storage policy 具体是什么策略?
  • 整理了几道前端面试题
  • 点控云智能客服:以AI重塑服务体验,登顶行业第一的革新之路
  • 餐饮营销:不是 “烧钱”,是 “递价值” 的落地术
  • 解释一下roberta,bert-chinese和bert-case有啥区别还有bert-large这些
  • ZeroMQ 编译 项目使用流程文档
  • 零知开源——基于STM32F103RBT6的智能风扇控制系统设计与实现
  • (GeSCD)Towards Generalizable Scene Change Detection论文精读(逐段解析)
  • A股大盘数据-20250905 分析
  • 代码版本控制
  • 学习心得分享
  • 【Cell Systems】SpotGF空间转录组去噪算法文献分享
  • 「数据获取」《中国包装业发展研究报告(2008)》
  • 禁止浏览器自动填充密码的方法
  • Vue 3 项目中引入 Iconify
  • 混合架构大型语言模型(Jamba)
  • Redis 的相关文件作用
  • Vulkan进阶系列11 - RenderPass 设置对渲染性能的影响
  • Java IO 流深度剖析:原理、家族体系与实战应用
  • Redis实战-附近的人实现的解决方案
  • MySQL数据库——事务、索引和视图
  • python-虚拟试衣
  • Doris 消费kafka消息
  • 并查集|栈
  • VMware替代 | ZStack生产级跨版本热升级等七大要素降低TCO50%
  • 2025年上半年前端技术圈生态总结
  • Vue基础知识-脚手架开发-任意组件通信-事件总线($bus)与消息订阅发布(pubsub-js)
  • python中等难度面试题(1)
  • 关于SFP(Small Form-factor Pluggable)模块的全面解析,从技术规格到市场应用的系统化说明:
  • LeetCode Hot 100 第11天