在Python中提供的一些接口中,一定不能少的肯定是创建Python应用线程的接口,倘若没有这个接口,编程还有什么太多的意义啊,对多线程的支持并非是没有代价的。
我们注意到boot->interp中保存了Python的PyInter- preterState对象,这个对象中携带了Python的module pool这样的全局信息,Python中所有的thread都会共享这些全局信息。关于代码清单所示的多线程环境的初始化动作。
有一点需要特别说明,当Python启动时,是并不支持多线程的。换句话说,Python中支持多线程的数据结构以及GIL都是没有创建的,Python之所以有这种行为是因为大多数的Python应用线程都不需要多线程的支持。
假如一个简单地统计词频的Python脚本中居然出现了多线程,面对这样的代码,我们一定都会抓狂的J。对多线程的支持并非是没有代价的。最简单的一点,如果激活多线程机制,而执行的Python程序中并没有多线程,那么在100条指令之后,Python虚拟机同样会激活线程的调度。
而如果不激活多线程,Python虚拟机则不用做这些无用功。所以Python选择了让用户激活多线程机制的策略。在Python虚拟机启动时,多线程机制并没有被激活,它只支持单线程,一旦用户调用thread.start_new_thread。
明确指示Python虚拟机创建新的线程,Python就能意识到用户需要多线程的支持,这个时候,Python虚拟机会自动建立多线程机制需要的数据结构、环境以及那个至关重要的GIL。
在这里,我们终于看到了Python中多线程机制的平台相关性,在Python25\Python目录下,有一大批thread_***.h这样的文件。在这些文件中,包装了不同操作系统的原生线程,并通过统一的接口暴露给Python,比如这里的PyThread_allocate_lock就是这样一个接口。
我们这里的thread_nt.h中包装的是Win32平台的原生thread,在本章中后面的代码剖析中,还会有大量与平台相关的代码,我们都以Win32平台为例。在PyThread_allocate_lock中,与PyEval_InitThreads非常类似的,它会检查一个initialized的变量,如果说GIL指示着Python的多线程环境是否已经建立。
那么这个initialized变量就指示着为了使用底层平台所提供的原生thread,必须的初始化动作是否完成。这些必须的初始化动作通常都是底层操作系统所提供的API,不同的操作系统可能需要不同的初始化动作。
一切真相大白了,原来,GIL(NRMUTEX)中的hevent就是Win32平台下的Event这个内核对象,而其中的thread_id将记录任一时刻获得GIL的线程的id。到了这里,Python中的线程互斥机制的真相渐渐浮出水面。
看来Python应用线程是通过Win32下的Event来实现了线程的互斥,熟悉Win32的朋友马上就可能想到,与这个Event对应的,必定有一个WaitForSingleObject。在PyEval_InitThreads通过PyThread_allocate_lock成功地创建了GIL之后,当前线程就开始遵循Python的多线程机制的规则:
在调用任何Python C API之前,必须首先获得GIL。因此PyEval_InitThreads紧接着通过PyThread_acquire_lock尝试获得GIL。最终,一个线程在释放GIL时,会通过SetEvent通知所有在等待GIL的hevent这个Event内核对象的线程,结合前面的分析。
如果这时候有线程在等待GIL的hevent,那么将被操作系统唤醒。这就是我们在前面介绍的Python将线程调度的第二个难题委托给操作系统来实现的机制。到了这时,调用PyEval_InitThread的线程(也就是Python主线程)已经成功获得了GIL,最后会调用PyThread_get_thread_ident()。
通过Win32的API:GetCurrent- ThreadId,获得当前Python主线程的id,并将其赋给main_thread,main_thread是一个静态全局变量。专职存储Python主线程的线程id,用以判断一个线程是否是Python主线程。最后,我们在给出整个PyEval_InitThread的函数调用关系。
值得注意的是,obj.done是一个Win32下的Semaphore内核对象,这个特殊的内核对象的用途我们马上就会看到。我们创建线程的工作需要func和arg,但是Win32下创建线程的API只允许用户指定一个自定义的参数,这就是需要用obj来打包的原因。
完成打包之后,调用Win32下创建thread的API:_beginthread来完成线程的创建。奇怪的是,我们期望的线程过程应该是thread1.py中定义的那个threadPoc呀,而这里指定的线程过程却是一个相当面生的bootstrap。实际上,在bootstrap中,会最终调用thread1.py中定义的threadProc。
但是,这里有一个至关重要的转折,还记得我们现在在哪里吗?没错,我们现在是沿着主线程的执行路径在剖析,而对bootstrap的调用并不是在主线程中发生的,而是在通过_beginthread所创建的子线程中发生的。从这里开始,我们需要特别注意代码的执行是在哪个线程中执行的,这对于理解Python应用线程机制相当重要。
好了,花开两朵,各表一枝。我们继续沿着主线程的执行路径前进。如果不出什么意外,_beginthread将最终成功地创建Win32下的原生线程,并顺利返回。在返回之后,主线程开始将自己挂起,等待obj.done。
我们前面看到,这是一个Win32的Semaphore内核对象。由于obj已经作为参数传递给了子线程,所以我们猜想,子线程会设置这个Semaphore,并最终唤醒主线程。现在我们来理清一下Python当前的状态。
Python当前实际上由两个Win32下的原生thread构成,一个是执行python程序(python.exe)时操作系统创建的主线程,另一个是我们通过thread1.py创建的子线程。主线程在执行PyEval_InitThread的过程中。
得了GIL,但是目前已经被挂起,这是为了等待子线程中控制着的obj.done。子线程的线程过程是bootstrap,不过我们刚才已经猜测了,从bootstrap出发,最终将在Python解释器中执行python1.py中定义的theadProc。但是,我们知道,子线程为了访问Python解释器,必须首先获得GIL,这是Python世界的游戏规则,谁也不能例外。
【编辑推荐】