python fork多线程进程时的坑

Python在fork多线程的进程时,创建的子进程只包含一个线程,该线程是调用fork函数的那个线程的副本。在man fork中,有The child process is created with a single thread—the one that called fork().这句话,亲测的确如此。在多线程进程中,为了多线程的同步及互斥,会有锁,在fork时,这些锁会一同fork到子进程中,这会导致一些问题,见下文。个人建议,最好不要fork多线程的进程,除非你有能力解决这个问题。在python的multiprocessing库中,就fork了多线程的进程。Queue中使用了线程将入队的消息放入管道,如果父进程使用了Queue.put(),那用Process()类创建子进程时,就会fork Queue类,但不会fork它里面的线程。multiprocessing的Process()创建子进程应该是解决上面提到的fork多线程进程时,锁相关的问题

import datetime
import time
import threading
import os
import thread
from multiprocessing import Process

def print_thread(*args):
    st = datetime.datetime.now()
    while True:
        now = datetime.datetime.now()
        print 'thread, now={}, tid={}, pid={}'.format(str(now), thread.get_ident(), os.getpid())
        time.sleep(1)
        if now - st > datetime.timedelta(minutes=30):
            break

def print_proc(*args):
    st = datetime.datetime.now()
    while True:
        now = datetime.datetime.now()
        print 'sub process, now={}, pid={}'.format(str(now), os.getpid(), os.getppid())
        time.sleep(1)
        if now - st > datetime.timedelta(minutes=30):
            break

if __name__ == '__main__':
    print 'main process, pid={}'.format(os.getpid())
    t = threading.Thread(target=print_thread)
    t.start()
    time.sleep(2)
    print 'create sub process'
    p = Process(target=print_proc)
    p.start()

执行结果如下:

main process, pid=5442
thread, now=2018-01-01 19:30:19.570559, tid=139746090014464, pid=5442
thread, now=2018-01-01 19:30:20.576551, tid=139746090014464, pid=5442
create sub process
thread, now=2018-01-01 19:30:21.584519, tid=139746090014464, pid=5442
sub process, now=2018-01-01 19:30:21.585514, pid=5448
thread, now=2018-01-01 19:30:22.586036, tid=139746090014464, pid=5442
sub process, now=2018-01-01 19:30:22.586514, pid=5448
thread, now=2018-01-01 19:30:23.587206, tid=139746090014464, pid=5442
sub process, now=2018-01-01 19:30:23.587485, pid=5448

原文:https://blog.codingnow.com/2011/01/fork_multi_thread.html

在 POSIX 标准中,fork 的行为是这样的:复制整个用户空间的数据(通常使用 copy-on-write 的策略,所以可以实现的速度很快)以及所有系统对象,然后仅复制当前线程到子进程。这里:所有父进程中别的线程,到了子进程中都是突然蒸发掉的。

其它线程的突然消失,是一切问题的根源。

我之前从未写过多进程多线程程序,不过公司里有 David Xu 同学(他实现维护着 FreeBSD 的线程库)是这方面的专家,今天跟徐同学讨论了一下午,终于觉得自己搞明白了其中的纠结。嗯,写点东西整理一下思路。

可能产生的最严重的问题是锁的问题。

因为为了性能,大部分系统的锁是实现在用户空间的。所以锁对象会因为 fork 复制到子进程中。

对于锁来说,从 OS 看,每个锁有一个所有者,即最后一次 lock 它的线程。

假设这么一个环境,在 fork 之前,有一个子线程 lock 了某个锁,获得了对锁的所有权。fork 以后,在子进程中,所有的额外线程都人间蒸发了。而锁却被正常复制了,在子进程看来,这个锁没有主人,所以没有任何人可以对它解锁。

当子进程想 lock 这个锁时,不再有任何手段可以解开了。程序发生死锁。

为何,POSIX 指定标准时,会定下这么一个显然不靠谱的规则?允许复制一个完全死掉的锁?答案是历史和性能。因为历史上,把锁实现在用户态是最方便的(今天依旧如此)。背后可能只需要一条原子操作指令即可。大多数 CPU 都支持的。fork 只管用户空间的复制,不会涉及其中的对象细节。

一般的惯例,多线程程序 fork 前,应该由发起 fork 的线程 lock 所有子进程可能用到的锁,fork 后,把它们一一 unlock 。当然,这样的做法就隐含了锁的次序。如果次序和平时不同,那么就会死锁。

不光是显式的使用锁,许多 CRT 函数也会间接的使用。比如 fprintf 这些文件操作。因为对 FILE * 的操作是依靠锁来达到线程安全的。最常见的问题是在子线程里调用 fprintf 写 log 。

除此之外,就是要小心一些不依赖锁的数据一致性问题了。比如若在父进程里另一个线程中操作一个链表,fork 发生时,因为其它线程的突然消失,这个链表就可能会因为只操作了一半而是不完整的数据。不过这一般不会是问题,或者可以归咎于对锁的处理。(多个线程,访问同一块数据。比如一条链表。就是需要加锁的)

你可能感兴趣的:(Python,python,开发语言,后端)