• 多线程程序中使用fork的问题


    多线程程序里不准使用fork

    UNIX上C++程序设计守则3

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

    在多线程程序里,在“自身以外的线程存在的状态 ”下一使用fork的话,就可能引起各种各样的问题.比较典型的例子就是,fork出来的子进程可能会死锁.请不要,在不能把握问题的原委的情况下就在多线程程序里fork子进程.

    能引起什么问题呢?

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

    1      void* doit(void*) {
    2
    3      static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    4
    5      pthread_mutex_lock(&mutex);
    6
    7      struct  timespec  ts = {10, 0}; // 10秒寝る
    8      nanosleep(&ts, 0);                  // 睡10秒
    9
    10    pthread_mutex_unlock(&mutex);
    11
    12    return 0;
    13
    14}
    15
    16 
    17
    18   int main(void) {
    19
    20   pthread_t t; 
    21
    22   pthread_create(&t, 0, doit, 0); 
    23
    24                                // 做成并启动子线程·
    25
    26    if (fork() == 0) {
    27
    28        
    29
    30        
    31
    32        //子进程
    33
    34        //在子进程被创建的瞬间,父的子进程在执行nanosleep的场合比较多
    35
    36        doit(0);
    37        return 0 ;
    38    }
    39
    40    pthread_join(t, 0); //
    41
    42                    // 等待子线程结束
    43
    44}
    45


    以下是说明死锁的理由.

    一般的,fork做如下事情
       1. 父进程的内存数据会原封不动的拷贝到子进程中
       2. 子进程在单线程状态下被生成

    在内存区域里,静态变量( 注释*2 )mutex的内存会被拷贝到子进程里.而且,父进程里即使存在多个线程,但它们也不会被继承到子进程里. fork的这两个特征就是造成死锁的原因.
    译者注: 死锁原因的详细解释 ---
        1. 线程里的doit()先执行.
        2. doit执行的时候会给互斥体变量mutex加锁.
        3. mutex变量的内容会原样拷贝到fork出来的子进程中(在此之前,mutex变量的内容已经被线程改写成锁定状态).
        4. 子进程再次调用doit的时候,在锁定互斥体mutex的时候会发现它已经被加锁,所以就一直等待,直到拥有该互斥体的进程释放它(实际上没有人拥有这个mutex锁).
        5. 线程的doit执行完成之前会把自己的mutex释放,但这是的mutex和子进程里的mutex已经是两份内存.所以即使释放了mutex锁也不会对子进程里的mutex造成什么影响.

    例如,请试着考虑下面那样的执行流程,就明白为什么在上面多线程程序里不经意地使用fork就造成死锁了( 注释*3 ).
    1.    在fork前的父进程中,启动了线程1和2
    2.    线程1调用doit函数
    3.    doit函数锁定自己的mutex
    4.    线程1执行nanosleep函数睡10秒
    5.    在这儿程序处理切换到线程2
    6.    线程2调用fork函数
    7.    生成子进程
    8.    这时,子进程的doit函数用的mutex处于”锁定状态”,而且,解除锁定的线程在子进程里不存在
    9.    子进程的处理开始
    10.   子进程调用doit函数
    11.   子进程再次锁定已经是被锁定状态的mutex,然后就造成死锁

    像 这里的doit函数那样的,在多线程里因为fork而引起问题的函数,我们把它叫做”fork-unsafe函数”.反之,不能引起问题的函数叫 做”fork-safe函数”.虽然在一些商用的UNIX里,源于OS提供的函数(系统调用),在文档里有fork-safety的记载,但是在 Linux(glibc)里当然!不会被记载.即使在POSIX里也没有特别的规定,所以那些函数是fork-safe的,几乎不能判别.不明白的话,作 为unsafe考虑的话会比较好一点吧.(2004/9/12追记)Wolfram Gloger说过,调用异步信号安全函数是规格标准,所以试着调查了一下,在pthread_atforkの这个地方里有” In the meantime(注释*5), only a short list of async-signal-safe library routines are promised to be available.”这样的话.好像就是这样.

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

    直到目前为止,已经写上了thread+fork是危险的,但是有一个特例需要告诉大家.”fork后马上调用exec的场合,是作为一个特列不会产生问题的”. 什么原因呢..? exec函数(注释*6)一 被调用,进程的”内存数据”就被临时重置成非常漂亮的状态.因此,即使在多线程状态的进程里,fork后不马上调用一切危险的函数,只是调用exec函数 的话,子进程将不会产生任何的误动作.但是,请注意这里使用的”马上”这个词.即使exec前仅仅只是调用一回printf(“I’m child process”),也会有死锁的危险.
    译者注:exec函数里指明的命令一被执行,改命令的内存映像就会覆盖父进程的内存空间.所以,父进程里的任何数据将不复存在.

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

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

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

    (2004/9/11 追记一些忘了写的东西)
    不用使用规避方法1的时候,在fork后不调用任何函数(printf等)就马上调用execl等,exec系列的函数.如果在程序里不使用”没有exec就fork”的话,这应该就是实际的规避方法吧.
    译者注:笔者的意思可能是把原本子进程应该做的事情写成一个单独的程序,编译成可执行程序后由exec函数来调用.

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

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

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

    *1:生成子进程的系统调用
    *2:全局变量和函数内的静态变量
    *3:如果使用Linux的话,查看pthread_atfork函数的man手册比较好.关于这些流程都有一些解释.
    *4:Solaris和HP-UX等
    *5:从fork后到exec执行的这段时间
    *6:≒execve系统调用
    *7:仅仅做四则演算的话就是fork-safe的
  • 相关阅读:
    CF1539 VP 记录
    CF1529 VP 记录
    CF875C National Property 题解
    CF1545 比赛记录
    CF 1550 比赛记录
    CF1539E Game with Cards 题解
    CF1202F You Are Given Some Letters... 题解
    vmware Linux虚拟机挂载共享文件夹
    利用SOLR搭建企业搜索平台 之九(solr的查询语法)
    利用SOLR搭建企业搜索平台 之四(MultiCore)
  • 原文地址:https://www.cnblogs.com/super119/p/2311861.html
Copyright © 2020-2023  润新知