C/C++编程:pthread_atfork函数(解决在多进程程序中fork可能造成的死锁问题)

1059 篇文章 288 订阅
pthread_atfork函数用于在fork前后处理线程互斥锁的状态,防止子进程继承父进程的锁导致死锁。在多线程程序中,当父进程调用fork创建子进程时,子进程会继承父进程的锁状态。通过在prepare阶段加锁,parent和child阶段解锁,确保子进程继承的锁是未锁定的,从而避免死锁问题。示例代码展示了如何使用pthread_atfork正确处理锁状态,以及不使用时可能导致的死锁情况。
摘要由CSDN通过智能技术生成

调用pthread_atfork是因为父进程在fork子进程之后,子进程也有了父进程的锁,一把锁怎么能两个人拥有呢?所以就需要调用pthread_atfork函数来处理这个问题(注整个回答是基于父进程拥有多个线程,如果是单个就不需要)

  • 在进行linux系统里开发时,经常会调用linux的系统函数fork来产生一个子进程,如果父子进程都没有用到pthread线程相关函数,则就不存在需要理解pthread_atfork的函数的必要。问题是有时候既要考虑多线程,又要考虑多进程,这个时候就要仔细理解pthread_atfork这个函数的作用了。
  • 在父进程调用fork函数派生子进程的时候,如果父进程创建了pthread的互斥锁(pthread_mutex_t)对象,那么子进程将自动继承父进程中互斥锁对象,并且互斥锁的状态也会被子进程继承下来:如果父进程中已经加锁的互斥锁在子进程中也是被锁住的,如果在父进程中未加锁的互斥锁在子进程中也是未加锁的。
  • 在父进程调用fork之前所创建的pthread_mutex_t对象会在子进程中继续有效,而pthread_mutex_t对象通常是全局对象,会在父进程的任意线程中被操作(加锁或者解锁),这样就无法通过简单的方法让子进程明确知道被继承的 pthread_mutex_t对象到底有没有处于加锁状态。因此 pthread线程库就有了 pthread_atfork 这个函数,该函数的原型声明如下:
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));

该函数通过3个不同阶段的回调函数来处理互斥锁状态。参数如下:

  • prepare:

    • 将在fork调用创建出子进程之前被执行,它可以给父进程中的互斥锁对象明明确确上锁。
    • 这个函数是在父进程的上下文中执行的,正常使用时,我们应该在此回调函数调用pthread_mutex_lock 来给互斥锁明明确确加锁,这个时候如果父进程中的某个线程已经调用pthread_mutex_lock给互斥锁加上了锁,则在此回调中调用 pthread_mutex_lock 将迫使父进程中调用fork的线程处于阻塞状态,直到prepare能给互斥锁对象加锁为止。
  • parent: 是在fork调用创建出子进程之后,而fork返回之前执行,在父进程上下文中被执行。它的作用是释放所有在prepare函数中被明明确确锁住的互斥锁。

  • child: 是在fork返回之前,在子进程上下文中被执行。和parent处理函数一样,child函数也是用于释放所有在prepare函数中被明明确确锁住的互斥锁。

函数成功返回0, 错误返回错误码。
在这里插入图片描述

通过这个函数可以确保子进程继承pthread_mutex_t对象处在未加锁状态。该函数的正常的用法如下

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
 
#define ERROR(err, msg) do { errno = err; perror(msg); exit(-1); } while(0)
 
int count = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
 
void prepare() {
  int err;
  printf("prepare: pthread_mutex_lock ...\n");
  err = pthread_mutex_lock(&lock);
  if (err != 0) ERROR(err, "prepare: pthread_mutex_lock failed");
}
 
void parent() {
  int err;
  printf("parent: pthread_mutex_unlock ...\n");
  err = pthread_mutex_unlock(&lock);
  if (err != 0) ERROR(err, "parent: pthread_mutex_unlock");
}
 
void child() {
  int err;
  printf("child: pthread_mutex_unlock ...\n");
  err = pthread_mutex_unlock(&lock);
  if (err != 0) ERROR(err, "child: pthread_mutex_unlock");
}
 
 
void* thread_proc(void* arg) {
  while(1) {
    pthread_mutex_lock(&lock);
    count++;
    printf("parent thread:  count:%d\n",count);
    sleep(10);
    pthread_mutex_unlock(&lock);
    sleep(1);
  }
  return NULL;
}
 
int main(int argc,char * argv[])
{
  int err;
  pid_t pid;
  pthread_t tid;
  pthread_create(&tid, NULL, thread_proc, NULL);
  err = pthread_atfork(prepare, parent, child);
  if (err != 0) ERROR(err, "atfork");
 
  sleep(1);
  printf("parent is about to fork ...\n");
  pid = fork();
  if (pid < 0) ERROR(errno, "fork");
  else if (pid == 0) {
    // child process
    int status;
  printf("child running\n");
    while(1) {
      pthread_mutex_lock(&lock);
      count ++;
      printf("child: count:%d\n",count);
      sleep(2);
      pthread_mutex_unlock(&lock);
      sleep(1);
    }
    exit(0);
  }
 
  pthread_join(tid, NULL);
}

运行 atfork 发现 main函数会在prepare回调中等待:prepare: pthread_mutex_lock,直到获得锁以后再执行子进程创建。

一个在多线程程序里fork造成死锁的例子:

// 一个在多线程程序里fork造成死锁的例子
// 一个输出示例:
/*
pid = 19445 Entering main ...
pid = 19445 begin doit ...
pid = 19447 begin doit ...
pid = 19445 end doit ...
pid = 19445 Exiting main ...
*/
#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;
	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;
}


ps aux |grep deadlock_test

在这里插入图片描述
解决做法的本质上就是在fork执行之前先调用prepare函数解锁,这样确保子进程调用doit时可以成功上锁。

#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;
}

void prepare(void)
{
	pthread_mutex_unlock(&mutex);
}

void parent(void)
{
	pthread_mutex_lock(&mutex);
}

int main(void)
{
	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;
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值