背景
当我们通过fork去创建子进程时,当父/子进程都涉及到锁的操作,可能会产生死锁。
代码样例
#include <iostream>
#include <mutex>
#include <unistd.h>
std::mutex m;
int main() {
std::cout << "main process begin" << std::endl;
m.lock();
int pid = fork();
if (pid == -1) {
std::cout << "fork failed" << std::endl;
return -1;
}
if(pid == 0){ // 子进程
m.lock();
std::cout << "child process run" << std::endl;
} else {
}
m.unlock();
while (true) {
}
return 0;
}
代码示例中,父进程持有锁m,然后通过fork进行进程的创建,这个时候子进程里也进行锁操作,这个时候子进程就会死锁在这里
根因
当我们通过fork创建子进程时,进程会继承父进程的内存空间(写时复制技术,copy-on-write),包括代码段,堆栈,堆和数据段。
在子进程中锁定m时,这个时候从父进程里继承的m的锁状态处于锁定状态,这是再去m.lock,那就会死锁。
一些解决方法
如果子进程能够访问到锁,那锁定前先解锁
if(pid == 0){ // 子进程
m.unlock(); // 锁定前,先解锁
m.lock();
std::cout << "child process run" << std::endl;
} else {
}
如果子进程不方便访问到锁,使用 pthread_atfork()
std::mutex m;
void child() {
m.unlock();
}
int main() {
pthread_atfork(nullptr, nullptr, child); // 三个参数分别时,prepare,parent,child
}
- prepare 处理器在 fork() 调用之前执行,通常用于获取那些需要在 fork() 期间保持的锁。
- parent 处理器在 fork() 调用之后,在父进程中执行,通常用于释放 prepare 处理器中获取的锁。
- child 处理器在 fork() 调用之后,在子进程中执行,也通常用于释放 prepare 处理器中获取的锁。
总结
- 我们要尽量在多线程程序中使用fork()
- 使用fork()后立即调用exec()
- 避免在持有锁时调用fork()
当然当我们编写多进程大型程序时,很难避免,特别是引用了一些三方库这些不受控的代码