102-多线程与 fork

在多线程程序中使用 fork,可能会导致一些意外:

  • 子进程中只剩下一个线程,它是父进程中调用 fork 的线程的副本构成。这意味着在多线程环境中,会导致“线程蒸发”,莫名奇妙的失踪!
  • 因为线程蒸发,它们所持有的锁也可能未释放,这将导致子进程在获取锁时进入死锁。

本文将验证这两个问题,并给出一个可行的解决方案。

1. 线程蒸发

如果在多线程环境中执行 fork,派生的子进程是单线程,子进程中的线程是由父进程中调用 fork 的那个线程的副本构成,而子进程中所有其它的线程会消失。

程序 th_fork 的功能是在父进程中创建一个线程,不断打印自己的 pid 和 ppid。创建完线程后,父进程在主线程中执行 fork,子进程每 2 秒打印一个点。

1.1 代码

// th_fork.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <errno.h>

#define PERR(err, msg) do { errno = err; perror(msg); exit(-1); } while(0)

void* fun(void* arg) {
  while(1) {
    printf("I'm %d, my father is %d\n", getpid(), getppid());
    sleep(1);
  }
  return NULL;
}

int main() {
  int err;
  pid_t pid;
  pthread_t tid;
  pthread_create(&tid, NULL, fun, NULL);

  puts("parent about to fork ...");
  pid = fork();
  if (pid < 0) PERR(errno, "fork");
  else if (pid == 0) {
    // child
    int status;
    err = pthread_join(tid, (void**)&status);
    if (err != 0) PERR(err, "pthread_join");
    while(1) {
      puts(".");
      sleep(2);
    }   
    exit(0);
  }

  pthread_join(tid, NULL);
}

1.2 编译和运行

$ gcc th_fork.c -o th_fork -lpthread
$ ./th_fork


这里写图片描述
图1 线程蒸发

从图 1 中可以看到,子进程中只有一个线程在打点,子进程的 fun 函数线程已经“蒸发”掉了。

另外,在子进程中调用了 pthread_join 函数并没有报错,看起来就好像是子进程中的 fun 函数线程已经正常结束了。

2. fork 死锁

如果在 fork 的时候,线程未释放持有的锁,将导致死锁。

程序 fork_lock 演示了这种情况。

2.1 代码

// fork_lock.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <errno.h>

#define PERR(err, msg) do { errno = err; perror(msg); exit(-1); } while(0)

int total = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;


void* fun(void* arg) {
  while(1) {
    pthread_mutex_lock(&lock);
    total++;
    puts("fun:  total++");
    // 时间稍稍长一点,在这个时候父进程发起 fork 就导致持锁线程蒸发
    sleep(5);
    pthread_mutex_unlock(&lock);
    sleep(1);
  }
  return NULL;
}

int main() {
  int err;
  pid_t pid;
  pthread_t tid;
  pthread_create(&tid, NULL, fun, NULL);
  // 推迟 1 秒,让线程进入临界区。
  sleep(1);
  puts("parent about to fork ...");
  pid = fork();
  if (pid < 0) PERR(errno, "fork");
  else if (pid == 0) {
    // child
    int status;
    while(1) {
      // 由于 fun 线程蒸发,子进程获取锁就会死锁
      puts("child require lock...");
      pthread_mutex_lock(&lock);
      total++;
      puts("child: total++");
      sleep(2);
      pthread_mutex_unlock(&lock);
      sleep(1);
    }   
    exit(0);
  }

  pthread_join(tid, NULL);
}

2.2 编译和运行

$ gcc fork_lock.c -o fork_lock -lpthread
$ ./fork_lock


这里写图片描述
图2 fork 死锁

图 2 中,子进程在请求锁后,再也没有回应,进入死锁。

2.3 解决方案

初始的解决方案是在 fork 前先请求所有的锁,然后再 fork,另外我们希望 fork 完成后,再对所有锁进行解锁。看起来像下面这样(假设程序中只用了三个互斥量):

pthread_mutex_lock(&lock1);
pthread_mutex_lock(&lock2);
pthread_mutex_lock(&lock3);

pid = fork();

pthread_mutex_unlock(&lock1);
pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock3);

if (pid < 0) {
  perror("fork");
}
else if (pid > 0) {
  ...
}
else if (pid == 0) {
  ...
}

因此,我们将 pid = fork() 那一行改为下面三行:

pthread_mutex_lock(&lock);
pid = fork();
pthread_mutex_unlock(&lock);

其它的地方不变,重新编译运行,程序正常。


这里写图片描述
图2 将 fork 放入临界区,程序正常

实际上,linux 为我们提供一更加方便的机制,让我们不用每次 fork 的时候都自己加锁——atfork 函数。

关于 atfork 函数,在下一篇文章讨论,其实它所做的事,和我们的解决方案是差不多的。

3. 总结

  • 理解 fork 导致的线程蒸发
  • 理解 fork 死锁问题何时出现
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值