有人可能没听过fork safety,但是这种情况确实也挺常见。比如,当我们使用uWSGI做WSGI Server的时候,如果采用prefork模型,那么默认情况下,uWSGI的master进程会先加载WSGI Application,然后fork出若干个子进程。此时,如果我们把PyMongo的MongoClient放在全局,那么就会导致uWSGI在fork之前,建立MongoClient实例,因为PyMongo是fork unsafety的,所以这种情况会出问题,更多详情请移步:pymongo: MongoClient opened before fork错误排解。
thread safety应该是耳熟能详的,本文主要参照了维基百科的Thread_safety进行讲解。在这里,也举一个线程不安全的例子:Python的Gearman客户端是非线程安全的,当多个线程并发地提交任务、读取响应的时候,就会出现协议解析相关的错误。
下面再看一个例子:
[root@iZj6chejzrsqpclb7miryaZ ~]# cat test.py
# coding: utf8
import os
from threading import Lock
import time
import sys
# 生成一个Lock对象,并在fork之前获取它
lock = Lock()
lock.acquire()
# fork出一个子进程
pid = os.fork()
if pid == 0:
# 子进程
print u"子进程:pid是%s" % os.getpid()
print u"子进程:尝试获取锁"
lock.acquire()
print u"子进程:成功获取锁"
print u"子进程:退出"
elif pid > 0:
# 父进程
print u"父进程:子进程pid是%d" % pid
time.sleep(2)
print u"父进程:释放锁"
lock.release()
time.sleep(3)
else:
sys.exit("fork() error")
[root@iZj6chejzrsqpclb7miryaZ ~]# python test.py
父进程:子进程pid是18248
子进程:pid是18248
子进程:尝试获取锁
父进程:释放锁
[root@iZj6chejzrsqpclb7miryaZ ~]# strace -p 18248
strace: Process 18248 attached
futex(0x1c19ec0, FUTEX_WAIT_PRIVATE, 0, NULL^Cstrace: Process 18248 detached
<detached ...>
通过strace,可以看到子进程死锁了。
为了解开这个疑惑,我们应该先看一下:fork()
函数的使用说明,其中有一段话:
The child process is created with a single thread—the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.
翻译过来就是,fork出来的子进程中,只有一个线程,就是调用fork()
函数的那个线程。父进程的整个虚拟地址空间都会被复制到子进程中,包含互斥变量、条件变量和其它线程对象的状态。为了解决由此导致的问题,可以使用pthread_at_fork()
函数。
回过头来看代码:在fork子进程的时候,主进程的主线程持有锁。因此,在fork之后,子进程继承了一把已经被获取的锁,然后在子进程中acquire这把锁,就会产生死锁,进而导致子进程HANG住。
需要额外说明的是:在一个进程退出之后,操作系统会扫描它有哪些子进程,这样的子进程被称为“孤儿进程”,操作系统会将“孤儿进程”过继给pid为1的那个进程(比如,在centos6中是init进程,在centos7中是systemd进程)。
这个例子说明了,如果父进程中持有一把锁,然后执行fork()
操作,是不安全的。
1,什么是线程安全
线程安全代码可以确保多个线程并发地操作共享数据结构时,它们的行为是正确的。实现线程安全的数据结构有多种策略,后面会逐一地进行介绍。
线程安全是这样一种特性:它允许运行在多线程环境下的代码,通过同步的方式,重新建立程序的真正的执行流和程序文本之间的一致性。
2,如何实现线程安全
为了避免竞态条件说明1,达到线程安全的目的,通常有两类方式: