muduo源码剖析——使用pthread_atfork避免多进程多线程并用中出现的死锁问题

本文探讨了在多进程和多线程并发编程中出现死锁的问题,并提供了一个具体的代码示例。通过分析示例代码,展示了如何利用pthread_atfork()函数解决死锁问题,确保子进程和子线程能够正常解锁并退出。

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)(voidvoid (*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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值