pthread_atfork函数解决多线程多进程死锁问题

最好不要同时使用多线程多进程,两者择其一。比如在多线程程序中调用fork容易出现死锁。下面取一个例子说明这种情况,先看死锁代码。

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

上面函数主线程先调用pthread_create()创建一个子线程执行doit(),doit()里面先加锁,睡眠2s;主线程睡眠1s后调用fork(),子进程会复制父进程的内存映像,此时全局变量mutex处于加锁的状态,所以子进程自己的mutex也是加锁的,此时子进程是独立运行的,也去执行doit(),在里面试图加锁,因为本来mutex已经加锁,而且根本没有人会来解锁,所以子进程就会死锁。

pthread_atfork()函数

#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));

pthread_atfork()在fork()之前调用。当调用fork时,内部创建子进程前在父进程中会调用prepare,内部创建子进程成功后,父进程会调用parent ,子进程会调用child。

#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>

void prepare(void)
{
    printf("pid = %d prepare ...\n", static_cast<int>(getpid()));
}

void parent(void)
{
    printf("pid = %d parent ...\n", static_cast<int>(getpid()));
}

void child(void)
{
    printf("pid = %d child ...\n", static_cast<int>(getpid()));
}


int main(void)
{
    printf("pid = %d Entering main ...\n", static_cast<int>(getpid()));

    pthread_atfork(prepare, parent, child);

    fork();

    printf("pid = %d Exiting main ...\n", static_cast<int>(getpid()));

    return 0;
}

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

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
    {
        doit(NULL);
    }
    pthread_join(tid, NULL);
    printf("pid = %d Exiting main ...\n", static_cast<int>(getpid()));

    return 0;

在执行fork()创建子进程之前,先执行prepare(),将子线程加锁的mutex解锁下,然后为了与doit()配对,在创建子进程成功后,父进程调用parent()再次加锁,这时父进程的doit()就可以接着解锁执行下去。而对于子进程来说,由于在fork()创建子进程之前,mutex已经被解锁,故复制的状态也是解锁的,所以执行doit()就不会死锁了

有关锁的小实验

没有加锁的情况下,解锁会不会有问题
加两次锁,会死锁,解两次呢