Python主线程问题进行研究讨论
Python主线程的问题这里我想在重申一下,对于那些从来没有学习过编程或者并非计算机专业的编程学习者而言,Python是最好的选择之一,下面文章进行学习介绍。
PyThread_acquire_lock有两种工作方式,通过函数参数waitflag来区分。这个waitflag指示当GIL当前不可获得时。是否进行等待,更直接地说,就是当前线程是否通过WaitForSingleObject将自身挂起,直到别的线程释放GIL,然后由操作系统将自己唤醒。
如果waitflag为0,Python会检查当前GIL是否可用,GIL中的owned是指示GIL是否可用的变量,在前面的InitializeNonRecursiveMutex中我们看到这个值被初始化为-1。Python会检查这个值是否为-1,如果是,则意味着GIL可用,必须将其置为0,当owned为0后,表示该GIL已经被一个线程占用,不再可用。
对于我们这里分析的调用PyEval_InitThread的主线程而言,由于在初始化GIL之后就调用PyThread_ acquire_lock申请GIL,到这时,并没有第二个线程被创建,所以主线程会轻而易举地获得GIL的使用权。
注意这里的检查和更新owned的操作是通过一个Win32的系统API――Interlocked- CompareExchange――来完成的。这个API是一个原子操作,其函数原形和功能如下。原形:InterlockedCompareExchange(PLONG dest, long exchange, long compared) 功能:如果*dest == compared,那么*dest = exchange
与InterlockedCompareExchange相同的,InterlockedIncrement也是一个原子操作,其功能是将mutex->owned的值增加1。从这里可以看到,当一个线程开始等待GIL时,其owned就会被增加1。显然我们可以猜测,当一个线程最终释放GIL时。
一定会将GIL的owned减1,这样当所有需要GIL的线程都最终释放了GIL之后,owned会再次变为-1,意味着GIL再次变为可用。为了清晰地展示这一点,我们现在就来看看PyThread_aquire_lock的逆运算,PyThread_release_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主线程。
值得注意的是,obj.done是一个Win32下的Semaphore内核对象,这个特殊的内核对象的用途我们马上就会看到。我们创建线程的工作需要func和arg,但是Win32下创建线程的API只允许用户指定一个自定义的参数,这就是需要用obj来打包的原因。
完成打包之后,调用Win32下创建thread的API:_beginthread来完成线程的创建。奇怪的是,我们期望的线程过程应该是thread1.py中定义的那个threadPoc呀,而这里指定的线程过程却是一个相当面生的bootstrap。实际上,在bootstrap中,会最终调用thread1.py中定义的threadProc。
现在我们来理清一下Python当前的状态。Python主线程当前实际上由两个Win32下的原生thread构成,一个是执行python程序(python.exe)时操作系统创建的主线程,另一个是我们通过thread1.py创建的子线程。