0 前言
一般情况下,要么只是用多线程并发,要么只使用多进程并发。
如果同时使用多进程、多线程,且结合锁的使用,可能会出现死锁。
因此,要养成良好的并发编程习惯,不要同时使用多进程、多线程,要么用多进程,要么用多线程。
1 多线程多进程并用中出现死锁的场景
1.1 代码示例
观察下面代码:
#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* doit(void* arg)
{
printf("pid = %d begin doit ...\n",static_cast<int>(getpid()));
pthread_mutex_lock(&mutex);
struct timespec ts = {2, 0};
nanosleep(&ts, NULL);
pthread_mutex_unlock(&mutex);
printf("pid = %d end doit ...\n",static_cast<int>(getpid()));
return NULL;
}
int main(void)
{
printf("pid = %d Entering main ...\n", static_cast<int>(getpid()));
pthread_t tid;
// 在main主线程/进程处创建一个新/子线程
// 注意:子线程会共享主线程的资源(注意是共享,不是复制,也就是mutex只有一份),包括主线程的互斥信号量mutex
pthread_create(&tid, NULL, doit, NULL);
struct timespec ts = {1, 0};
nanosleep(&ts, NULL);
// 在main主进程处创建一个新/子进程
// 注意:子进程会复制一份主进程的资源(注意是复制),包括主进程的互斥信号量mutex
if (fork() == 0)
{
doit(NULL);
}
pthread_join(tid, NULL);
printf("pid = %d Exiting main ...\n",static_cast<int>(getpid()));
return 0;
}
1.2 程序运行结果
程序的运行结果如下:
pid = 19445 Entering main ...
pid = 19445 begin doit ...
pid = 19447 begin doit ...
pid = 19445 end doit ...
pid = 19445 Exiting main ...
子线程19445成功解锁并退出
子进程19447陷入死锁,无法退出
1.3 原因分析
参考下图。

- t1时刻:主线程main创建子线程T’,子线程T’共享主线程的互斥信号量mutex,此时主线程main和子进程T’的mutex都满足mutex
= 1; - t2时刻:主线程/父进程main创建子进程P’,子进程P’复制一份父进程main的互斥信号量mutex,此时mutex = 0;
- 因此子进程P’会陷入等待(等待mutex恢复1,这时子进程P’才能申请mutex锁变量进入临界区),等待有人来解锁,然而子进程所在的进程中没有其他方法将mutex恢复1,因此子进程P’会一直等待,从而导致死锁。
2 使用pthread_atfork()
2.1 pthread_atfork()函数作用详解
函数原型:
pthread_atfork(void (*prepare)(void,void (*parent)(void, void(*child)(void))
- prepare回调函数在父进程fork创建子进程之前调用;
- child回调函数在fork返回之前在子进程环境中调用;
- parent回调函数在fork创建了子进程以后,但在fork返回之前在父进程的进程环境中调用的;
2.2 使用pthread_atfork()代码示例
#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* doit(void* arg)
{
printf("pid = %d begin doit ...\n",static_cast<int>(getpid()));
pthread_mutex_lock(&mutex);
struct timespec ts = {2, 0};
nanosleep(&ts, NULL);
pthread_mutex_unlock(&mutex);
printf("pid = %d end doit ...\n",static_cast<int>(getpid()));
return NULL;
}
// 添加prepare和paren函数
void prepare(void)
{
pthread_mutex_unlock(&mutex);
}
void parent(void)
{
pthread_mutex_lock(&mutex);
}
int main(void)
{
// 设置pthread_atfork
pthread_atfork(prepare, parent, NULL);
printf("pid = %d Entering main ...\n", static_cast<int>(getpid()));
pthread_t tid;
pthread_create(&tid, NULL, doit, NULL);
struct timespec ts = {1, 0};
nanosleep(&ts, NULL);
if (fork() == 0)
{
doit(NULL);
}
pthread_join(tid, NULL);
printf("pid = %d Exiting main ...\n",static_cast<int>(getpid()));
return 0;
}
2.3 程序运行结果
pid = 31443 Entering main ...
pid = 31443 begin doit ...
pid = 31445 begin doit ...
pid = 31443 end doit ...
pid = 31443 Exiting main ...
pid = 31445 end doit ...
pid = 31445 Exiting main ...
子进程、子线程都能正常解锁、退出
2.4 原因分析
- t1时刻:主线程main创建子线程T’,子线程T’共享主线程的互斥信号量mutex,此时主线程main和子进程T’的mutex都满足mutex
= 1; - (t2调用fork()之前,由于pthread_atfork(prepare, parent, NULL)的设置,prepare函数已经将mutex进行了解锁,此时父进程的mutex恢复为1,即mutex = 1)
void prepare(void)
{
pthread_mutex_unlock(&mutex);
}
- t2时刻:主线程/父进程main创建子进程P’,子进程P’复制一份父进程main的互斥信号量mutex,此时mutex = 1;
- 因此这时子进程P’不会陷入死锁,可以完成正常的加锁、解锁操作。
Note:
为什么parent函数中要再加一次锁?
void parent(void)
{
pthread_mutex_lock(&mutex);
}
因为主线程main和子线程T’共享mutex,而调用fork()之前,pepare()函数中为主线程main中的mutex解锁,此时mutex又由0变回了1。因此需要在调用fork()之后,通过parent()函数调用lock将mutex重新变回0,这样子进程T’在unlock解锁时,又能将mutex恢复为1。
可以理解为lock和unlock的次数此处要匹配,prepare()函数将mutex解锁unlock一次,需要在parent()函数中再lock一次。
3 参考材料
1 https://blog.csdn.net/codinghonor/article/details/43737869
2 https://www.bilibili.com/video/BV11b411q7zr?p=13
本文探讨了在多进程和多线程并发编程中出现死锁的问题,并提供了一个具体的代码示例。通过分析示例代码,展示了如何利用pthread_atfork()函数解决死锁问题,确保子进程和子线程能够正常解锁并退出。

7208

被折叠的 条评论
为什么被折叠?



