首页 > 代码库 > 多线程程序与fork()

多线程程序与fork()

前言:exce调用并不创建新进程,所以前后的进程ID并未改变,exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段

 

多线程程序里不准使用fork :为什么???

UNIXC++程序设计守则3

 

准则3:多线程程序里不准使用fork

 

能引起什么问题呢?

 

那看看实例吧.一执行下面的代码,在子进程的执行开始处调用doit(),发生死锁的机率会很高.

 

void* doit(void*) {

    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

    pthread_mutex_lock(&mutex);

    struct timespec ts = {10, 0}; nanosleep(&ts, 0); // 10秒寝る

                                                     // 10

    pthread_mutex_unlock(&mutex);

    return 0;

}

int main(void) {

        pthread_t t;

        pthread_create(&t, 0, doit, 0);                                 // 做成并启动子线程

        if (fork() == 0) {

              //子进程

             //在子进程被创建的瞬间,父的子进程在执行nanosleep的场合比较多

              doit(0);

              return 0;

        }

        pthread_join(t, 0); //

         // 等待子线程结束

}

 

 

以下是说明死锁的理由:

一般的,fork做如下事情

   1. 父进程的内存数据会原封不动的拷贝到子进程中

   2. 子进程在单线程状态下被生成

 

在内存区域里,静态变量mutex的内存会被拷贝到子进程里.而且,父进程里即使存在多个线程,但它们也不会被继承到子进程里(锁状态被继承,而线程则不被继承,当然子进程的主线程可以看做一个叫进程)fork的这两个特征就是造成死锁的原因.

译者注死锁原因的详细解释 ---

   1. 线程里的doit()先执行.

  2. doit执行的时候会给互斥体变量mutex加锁.

   3. mutex变量的内容会原样拷贝到fork出来的子进程中(在此之前,mutex变量的内容已经被线程改写成锁定状态).

   4.子进程再次调用doit的时候,在锁定互斥体mutex的时候会发现它已经被加锁,所以就一直等待,直到拥有该互斥体的进程释放它(实际上没有人拥有这个mutex).

    5.线程的doit执行完成之前会把自己的mutex释放,但这是的mutex和子进程里的mutex已经是两份内存.所以即使释放了mutex锁也不会对子进程里的mutex造成什么影响.

 

 

随便说一下,malloc函数就是一个维持自身固有mutex的典型例子,通常情况下它是fork-unsafe.依赖于malloc函数的函数有很多,例如printf函数等,也是变成fork-unsafe.

 

直到目前为止,已经写上了thread+fork是危险的,但是有一个特例需要告诉大家.fork后马上调用exec的场合,是作为一个特列不会产生问题的”什么原因呢..?exec函数*6一被调用,进程的”内存数据”就被临时重置成非常漂亮的状态.

 

因此,即使在多线程状态的进程里,fork后不马上调用一切危险的函数,只是调用exec函数的话,子进程将不会产生任何的误动作.但是,请注意这里使用的”马上”这个词.即使exec前仅仅只是调用一回printf(Im child process),也会有死锁的危险.

 

译者注:exec函数里指明的命令一被执行,该命令的内存映像就会覆盖父进程的内存空间.所以,父进程里的任何数据将不复存在.

子进程在创建后,是写时复制的,也就是子进程刚创建时,与父进程一样的副本,当exce后,那么老的地址空间被丢弃,而被新的exec的命令的内存的印像覆盖了进程的内存空间,所以锁的状态无关紧要了。

 

如何规避灾难呢?

为了在多线程的程序中安全的使用fork,而规避死锁问题的方法有吗?试着考虑几个.

 

规避方法1:fork的时候,在它之前让其他的线程完全终止.

fork之前,让其他的线程完全终止的话,则不会引起问题.但这仅仅是可能的情况.还有,因为一些原因而其他线程不能结束就执行了fork的时候,就会是产生出一些解析困难的不具合的问题.

 

规避方法2:fork后在子进程中马上调用exec函数

 

不用使用规避方法1的时候,fork后不调用任何函数(printf)就马上调用execl,exec系列的函数.

译者注:笔者的意思可能是把原本子进程应该做的事情写成一个单独的程序,编译成可执行程序后由exec函数来调用.

 

规避方法3:”其他线程”中,不做fork-unsafe的处理

除了调用fork的线程,其他的所有线程不要做fork-unsafe的处理.为了提高数值计算的速度而使用线程的场合*7,这可能是fork-safe的处理,但是在一般的应用程序里则不是这样的.即使仅仅是把握了那些函数是fork-safe,做起来还不是很容易的.fork-safe函数,必须是异步信号安全函数,而他们都是能数的过来的.因此,malloc/new,printf这些函数是不能使用的.

 

规避方法4:使用pthread_atfork函数,在即将fork之前调用事先准备的回调函数.apue中详细介绍了它

使用pthread_atfork函数,在即将fork之前调用事先准备的回调函数,在这个回调函数内,协商清除进程的内存数据.但是关于OS提供的函数(:malloc),在回调函数里没有清除它的方法.因为malloc里使用的数据结构在外部是看不见的.因此,pthread_atfork函数几乎是没有什么实用价值的.

 

规避方法5:在多线程程序里,不使用fork

就是不使用fork的方法.即用pthread_create来代替fork.这跟规避策2一样都是比较实际的方法,值得推荐.

多线程程序与fork()