多线程环境下fork的使用以及锁的变化

关于线程的创建与使用,前几个博客说的很详细了,那么我们如果在某一个函数线程中调用fork函数有什么需要注意的吗?

一:线程中使用fork函数

在线程中调用 fork 函数,子进程只会启用调用 fork 函数的那条线程,其他线程不会启用。

多线程环境下fork的使用以及锁的变化_第1张图片

示例代码:

#include
#include
#include
#include
#include
#include
//测试线程中调用frok的结果
void *fun(void *arg)
{
	int i=0;
	int n=fork();
	if(n==0)
	{
		for(;i<4;i++)
		{
			sleep(1);
			printf("%d pig heng heng heng\n",getpid());
		}
	}
	else
	{
		for(;i<3;i++)
		{
			sleep(1);
			printf("%d cat miao miao miao\n",getpid());
		}
	}
}

int main()
{
	pthread_t pthid;
	int res=pthread_create(&pthid,0,fun,0);
	assert(res==0);

	int i=0;
	for(;i<5;i++)
	{
		printf("%d dog wang wang wang\n",getpid());
		sleep(1);
	}
	pthread_exit(NULL);
}

执行结果:

多线程环境下fork的使用以及锁的变化_第2张图片

可以看出主进程中的输出并没有被再次执行,只打印了5次dog,子进程的确只调用了使用fork的函数线程

二:fork函数后锁的继承问题

子进程会继承其父进程的锁以及锁的状态,但是父子进程用的不是同一把锁,父进程解锁并不会影响到子进程的锁,即fork以后子进程和父进程使用的是两把锁。那么子进程就有可能死锁,比如我们在fork之前,一个线程对某个锁进行的lock操作,即持有了该锁,然后另外一个线程调用了fork创建子进程。可是在子进程中持有那个锁的线程却"消失"了,从子进程的角度来看,这个锁被“永久”的上锁了,因为它的持有者“蒸发”了,所以如果子进程中的这个线程对这个已经被持有的锁进行lock操作的话,就会发生死锁。

多线程环境下fork的使用以及锁的变化_第3张图片

可能有人会想到可以在fork之前让调用fork的线程获取所有的锁,然后再在fork出的子进程的中释放每一个锁。这种方法是可以的,但是这种做法会带来一个问题,那就是隐含了一种上锁和解锁的先后顺序,如果次序不同,就会发生死锁。

多线程环境下fork的使用以及锁的变化_第4张图片

比如,子进程继承来了父进程中的锁A和锁B,现在需要在自己的线程中对其解锁(当然也是在自己的线程中加锁的,我没有画),如果向我上面那种画法,就会出现死锁。因为先对A加锁然后对B加锁,那么你在对B进行解锁前应该先对A解锁,但是对A解锁在对B解锁之后,所以会发生死锁。所以这种方法很麻烦,但是有人不厌其烦说可以做到控制顺序,但是这种线程中调用fork还是有很多问题是不能控制的(比如库函数等),所以系统为我们解决了这种问题,提供给了我们一个函数

int thread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));

  • prepare处理函数由父进程在fork创建子进程前调用,这个函数的任务是获取父进程定义的所有锁。
  • parent处理函数是在fork创建了子进程以后,但在fork返回之前在父进程环境中调用的。它的任务是对prepare获取的所有锁解锁。
  • child处理函数在fork返回之前在子进程环境中调用,与parent处理函数一样,它也必须解锁所有prepare中所获取的锁。

上述并不是加锁一次解锁了两次,而是各自独自解锁。因为子进程地址空间创建的时候,得到了所有父进程定义的锁的副本,继承的是父进程的锁的拷贝。任何一个进程修改锁的状态就会为另一个进程拷贝一份滴(写时拷贝),所以并没有加锁一次解锁两次,这也是我上面说的父子进程用的不是同一把锁,各自操作各自的锁就没有影响啦。

需要注意的是pthread_atfork只能清理锁,但不能清理条件变量。在有些系统的实现中条件变量不需要清理。但是在有的系统中,条件变量的实现中包含了锁,这种情况就需要清理。但是目前并没有清理条件变量的接口和方法。

测试用例:

#include
#include
#include
#include
#include
#include
//使用pthread_atfork函数进行获取锁
pthread_mutex_t mutex;
void *fun(void *arg)
{
	printf("fun will get pthread\n");
	pthread_mutex_lock(&mutex);
	printf("fun mutex locked\n");
	sleep(5);
	pthread_mutex_unlock(&mutex);
	printf("fun mutex unlcok\n");
}
void prepare()   //pthread_atfork一旦被调用就会执行该函数,但是需要等待fun释放锁才可以将锁加上
{
	pthread_mutex_lock(&mutex);
	printf("prepare get mutex\n");
}
void parent()
{
	pthread_mutex_unlock(&mutex);
	printf("parent progress get mutex\n");
}
void child()
{
	pthread_mutex_unlock(&mutex);
	printf("child progress get mutex\n");
}
int main()
{
	pthread_mutex_init(&mutex,NULL);
	pthread_t id;
	int res=pthread_create(&id,NULL,fun,NULL);
	assert(res==0);
	sleep(1);

	pthread_atfork(prepare,parent,child);
	if(fork())   //父进程与子进程相当与加了两把不同的锁了,没有什么关系了
	{
		printf("father will get mutex\n");
		pthread_mutex_lock(&mutex);
		sleep(1);
		printf("father lock success\n");
		pthread_mutex_unlock(&mutex);
		printf("father over\n");
	}
	else
	{
		printf("child will get mutex\n");
		pthread_mutex_lock(&mutex);
		printf("child lock success\n");
		sleep(2);
		pthread_mutex_unlock(&mutex);
		printf("child over\n");
	}
}

 多线程环境下fork的使用以及锁的变化_第5张图片

指定在fork调用之前,建子进程之前,调用prepare函数,获取所有的锁,然后创建子进程,子进程创建以后,父进程环境中调用parent所有的锁,子进程环境中调用child解所有的锁,然后fork函数再返回。这样保证了fork之后,子进程拿到的锁都是解锁状态,避免死锁。

你可能感兴趣的:(多线程环境下fork的使用以及锁的变化)