上回书说完了信号和tick,这回书接着来说check。
当某一个主线程运行了100tick之后,经过检查,从而释放了gil锁,并由操作系统调度其他的线程来运行,如图是线程2开始运行,当线程2也运行完100个tick之后,调度线程3.这两次进行的都很顺利,但是线程三在运行时,却出现故障。
如前所述,只有主线程中实现了线程处理函数,可以处理系统广播的信号,而在线程三的这一次运行中,当线程3接收到系统传递过来的信号后,线程3不再等100个tick才去检查,并释放gil,而是每过一个tick都进行一次检查,这样实现的目的是为了尽快地能够将执行转移给主线程,从而能够尽快地响应系统的发出的信号。
但是:
python是没有调度权的
在运行check,进行gil锁的释放后,操作系统会调试那个线程来运行,并不是python来决定的,于是当存在挂起线程的情况下,python解释能够做的惟一的事情,就是频繁地切换线程,从而期待主线程能够运行并处理挂起的信号。
于是就出现了图中的情况,只要存在挂起的信号,并存在主线程的情况下,其他线程频繁地在每一个tick执行后进行线程切换(也就是阻塞自己,把调试权交给系统),同时释放gil锁,并再次试图获取gil锁。
就算是在单核cpu中,多线程程序也运行的如此毛线,只要有系统中挂起的信号,python多线程程序的执行就会很神经。
总算说完了check ,tick 和signal,看官可能比较疑惑,这些和最早先的故事有什么关系。
不要着急,最毛线的地方在这里。
所谓gil,是python中符合POSIX标准的一个锁的实现,一般是一个信号量,或者pthread条件变量来实现的,下图即是此条件变量的明细
条件变量,以及gil的等待队列。
在使用了这样一个实现的基础上,最要命的地方来了,在python中,获取gil与释放gil的方式,居然同样是通过发送信号来实现的。
在信号的发送与接收中,一个执行的线程在发送信号后,需要与系统内核通信,对系统内核对信号进行一些处理,然后再系统唤醒被调度的线程,被调试的线程获取gil,从而既获得调度,又获利gil,这样就能够执行了。
这个过程需要一定的时间。
这就导致了所谓一gil战争。
作者dabeaz(python cookbook一书的作者)很强大,下载了python的源码之后,修改了其gil的运行机制,使得python在运行中,对gil的状态进行了记录,并在退出运行时dump内存到硬盘上,记录的格式如下所示:
当python多线程程序运行在单核cpu上的时候,情况尚好,当一个线程经过100tick之后,就阻塞自己,并向系统发送信号
,系统在经过处理后,将信号发送给其他的线程,其他的线程获得gil,同时也获得系统的调度,就能够运行了。
但是在多核cpu上,就会出现一个很无稽的情况,线程给系统发了信号,系统将在信号发给其他的线程,其他线程被调度运行,但是在这期间,线程A并没有被阻塞,因为。这是多核cpu!!!
系统一看,丫这里还有个核啊,线程A你不用阻塞掉,于是将线程A换了一个核,继续运行,而线程A继续后第一件事情就是再次地抢夺gil,在这个时间,线程B还没调度结束,结果就是线程A获得了gil,继续运行下去。
这样的结果,就是,线程B获得了系统的调试,但是却没有获得gil锁,于是就不断地试图地抢夺gil锁,而线程A一直拿着锁不给线程A,线程A过段时间释放次gil,也很快地被系统调度再次运行从而很快地利获得gil锁,线程B于是一直被唤醒,抢夺,得不到,再次阻塞。。。。
到这里,gil战争的故事,就算讲完了。
这种毛线的tick机制,一直持续到了python3.2,在python3.2后,gil锁终于有许多年来一次改进,将tick机制改为时间计算。
不过那又是另外的一个故事了。
而且,目前流行的python版本,仍然是python2.6 或者python2.7,因此,gil锁,仍然将长期困扰人们。