现在说明一下关于Python虚拟机的状态问题,实际上Python当前由两个原生thread构成,一个是主线程执行程序(python.exe)时操作系统创建的,另一个是通过thread1.py创建的子线程。
在代码清单15-1的[1]中,我们注意到boot->interp中保存了Python虚拟机的PyInter- preterState对象,这个对象中携带了Python的module pool这样的全局信息,Python中所有的thread都会共享这些全局信息。
关于代码清单15-1的[2]处所示的多线程环境的初始化动作,有一点需要特别说明,当Python启动时,是并不支持多线程的。换句话说,Python中支持多线程的数据结构以及GIL都是没有创建的,Python之所以有这种行为是因为大多数的Python程序都不需要多线程的支持。
- [threadmodule.c]
- static PyObject* thread_PyThread_start_new_thread(PyObject *self, PyObject
- *fargs)
- {
- PyObject *func, *args, *keyw = NULL;
- struct bootstate *boot;
- long ident;
- PyArg_UnpackTuple(fargs, "start_new_thread", 2, 3, &func, &args, &keyw);
- //[1]:创建bootstate结构
- boot = PyMem_NEW(struct bootstate, 1);
- boot->interp = PyThreadState_GET()->interp;
- boot->funcfunc = func;
- boot->argsargs = args;
- boot->keywkeyw = keyw;
- //[2]:初始化多线程环境
- PyEval_InitThreads(); /* Start the interpreter's thread-awareness */
- //[3]:创建线程
- ident = PyThread_start_new_thread(t_bootstrap, (void*) boot);
- return PyInt_FromLong(ident);
- [thread.c]
- /* Support for runtime thread stack size tuning.
- A value of 0 means using the platform's default stack size
- or the size specified by the THREAD_STACK_SIZE macro. */
- static size_t _pythread_stacksize = 0;
- [thread_nt.h]
- long PyThread_start_new_thread(void (*func)(void *), void *arg)
- {
- unsigned long rv;
- callobj obj;
- obj.id = -1; /* guilty until proved innocent */
- obj.func = func;
- obj.arg = arg;
- obj.done = CreateSemaphore(NULL, 0, 1, NULL);
- rv = _beginthread(bootstrap, _pythread_stacksize, &obj); /* use default stack size */
- if (rv == (unsigned long)-1) {
- //创建raw thread失败
- obj.id = -1;
- }
- else {
- WaitForSingleObject(obj.done, INFINITE);
- }
- CloseHandle((HANDLE)obj.done);
- return obj.id;
- }
假如一个简单地统计词频的Python脚本中居然出现了多线程,面对这样的代码,我们一定都会抓狂的J。对多线程的支持并非是没有代价的,最简单的一点,如果激活多线程机制。
而执行的Python程序中并没有多线程,那么在100条指令之后,Python虚拟机同样会激活线程的调度。而如果不激活多线程,Python虚拟机则不用做这些无用功。所以Python选择了让用户激活多线程机制的策略。在Python虚拟机启动时,多线程机制并没有被激活,它只支持单线程,一旦用户调用thread.start_new_thread。
明确指示Python虚拟机创建新的线程,Python就能意识到用户需要多线程的支持,这个时候,Python虚拟机会自动建立多线程机制需要的数据结构、环境以及那个至关重要的GIL。在这里,我们终于看到了Python中多线程机制的平台相关性,在Python25\Python目录下,有一大thread_***.h这样的文件,在这些文件中,包装了不同操作系统的原生线程。#t#
并通过统一的接口暴露给Python,比如这里的PyThread_allocate_lock就是这样一个接口。我们这里的thread_nt.h中包装的是Win32平台的原生thread,在本章中后面的代码剖析中,还会有大量与平台相关的代码。
我们都以Win32平台为例。一切真相大白了,原来,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。
与InterlockedCompareExchange相同的,InterlockedIncrement也是一个原子操作,其功能是将mutex->owned的值增加1。从这里可以看到,当一个线程开始等待GIL时,其owned就会被增加1。显然我们可以猜测,当一个线程最终释放GIL时,一定会将GIL的owned减1,这样当所有需要GIL的线程都最终释放了GIL之后,owned会再次变为-1,意味着GIL再次变为可用。
为了清晰地展示这一点,我们现在就来看看PyThread_aquire_lock的逆运算,PyThread_release_lock每一个将从运行转态转为等待状态的线程都会在被挂起之前调用它以释放对GIL的占有。
【编辑推荐】