Python多线程机制.doc_第1页
Python多线程机制.doc_第2页
Python多线程机制.doc_第3页
Python多线程机制.doc_第4页
Python多线程机制.doc_第5页
免费预览已结束,剩余44页可下载查看

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

Python多线程机制开发多线程的应用系统,是在日常的软件开发中经常会遇到的需求。现在的编程语言都为多线程开发提供了很好的支持,无论是通过库的支持还是将多线程机制内建在语言之中。Python也为多线程系统的开发提供了很好的支持。同样身为动态语言,Ruby也提供了多线程的支持,但是在 Ruby 1.9之前的多线程机制是在语言的实现中模拟了线程及线程调度机制,而并没有使用操作系统本身的线程机制(在以后的描述中,我们称为原生线程)。Ruby 1.9中整合了YARV作为Ruby新的虚拟机,在YARV中,将操作系统的原生线程引入了Ruby。每一个Ruby线程都对是操作系统上的一个线程,在 Ruby内部,维护着一个全局资源锁,一个Ruby线程必须首先获得这个锁,才能成为活动的线程,从而使用Ruby虚拟机的全局资源。这一切,在Python中早已实现,Python中的线程从一开始就是操作系统的原生线程,而Python虚拟机也同样使用一个全局解释器锁(Global Interpreter Lock,GIL)来互斥线程对Python虚拟机的使用。15.1 GIL与线程调度为了理解Python为什么需要Global Interpreter Lock(GIL),考虑这样的情形:假设有两个线程A、B,在两个线程中,都同时保存着对内存中同一对象obj的引用,也就是说,这时 obj-ob_refcnt的值为2。如果A销毁对obj的引用,显然,A将通过Py_DECREF调整obj的引用计数值。我们知 道,Py_DECREF的整个动作可以分为两个部分: -obj-ob_refcnt; if(obj-ob_refcnt = 0) destory object and free memory。如果A在执行完第一个动作之 后,obj-ob_refcnt的值变为1。不幸的是,恰恰在这个时候,线程调度机制将A挂起,而唤醒了B。更为不幸的是,B同样也开始销毁对 obj的引用。B完成第一个动作之后,obj-ob_refcnt为0,B是一个幸运儿,它没有被线程调度打断,而是顺利地完成了接下来的第二个 动作,将对象销毁,内存释放。好了,现在A又被重新唤醒,可惜现在已经物是人非,obj-ob_refcnt已经被B减少到0,而不是当初的1。 按照约定,傻乎乎的A开始再一次地对已经销毁的对象进行对象销毁和内存释放的动作。这样的结局是什么?只有天知道。为了支持多线程机制,一个基本的要求就是需要实现不同线程对 共享资源访问的互斥,Python也不例外,这正是引入GIL的根源所在。Python中的GIL是一个非常霸道的互斥实现,正如它的名字所暗示 的,GIL是一个解释器(Interpreter)为了呼应GIL中的Interpreter,本章中,我们会以解释器来称呼虚拟机级的互斥机 制,也就是说,在一个线程拥有了解释器的访问权之后,其他的所有线程都必须等待它释放解释器的访问权,即使这些线程的下一条指令并不会互相影响。初看上 去,这样的保护机制粒度太大了,我们似乎只需要将可能被多个线程共享的资源保护起来即可,对于不会被多个线程共享的资源,完全可以不用保护。实际上,在 Python的发展历史中,确实出现过这样的解决方案,但是令人惊奇的,这样的方案在单处理器上的多线程实现的效率上却没有GIL的方案好。所以现在 Python中的多线程机制是在GIL的基础上实现的。当然,这样的方案也意味着,无论如何,在同一时间,只能有一 个线程能访问Python所提供的API。注意这里的同一时间对于单处理器是毫无意义的,因为单处理器的本质是不可能并行的,但是对于多处理器,情形就完 全不同了,同一时间,确实可以有多个线程独立运行,然而Python的GIL限制了这样的情形,使得多处理器最终退化为单处理器,性能大打折扣。这一点其 实早已被Python社区所认识,也进行了大量的探索。大约在99年的时候,Greg Stein和Mark Hammond两位老兄基于Python 1.5创建了一份去除GIL的branch,但是很不幸,这个分支在很多基准测试上,尤其是单线程操作的测试上,效率只有使用GIL的Python的一半 左右。因为细粒度的锁机制会导致大量的加锁、解锁的操作,而加锁、解锁对于操作系统来说,是一个比较重量级的动作;另一方面,没有了GIL的保护,编写 Python扩展模块的难度大大增加。所以,到目前Python的最新版本2.5为止,GIL仍然是多线程机制的基石,而我们也仍然将视线集中在单处理器 上。实际上,在去年5月份Python 3000的邮件列表上,Python的创造者,Guido,提出了一个比较可行的的解决方案,在多处理器的情况下,完全可以创建多个Python进程,充 分使用多处理器,进程之间通过IPC的方式进行通信。当然,Guido也仅仅是提出了这么一个想法,并没有太多的实现细节透露出来。从之前的分析中,我们知道,对于Python而言,字节码解 释器是Python的核心所在,所以Python通过GIL来互斥不同线程对解释器的使用。在图15-1中,三个拟人化的线程A,B和C都需要使用解释器 来执行字节码,以完成某种计算,但是在这之前,它们必须获得GIL,因为GIL把守着通往字节码解释器的大门。当某个线程(A)获得了GIL之后,其他的 两个线程(B,C)只能等待A释放GIL之后,然后才能进入解释器,执行一些计算。实际上,Python的GIL背后所保护的不仅仅是Python的解释 器,同样还有Python的C API,在C/C+和Python的混合开发中,在涉及到原生线程和Python线程的相互协作时,也需要通过GIL进行互斥。关于这一点,我们将在后 面详细阐述。那么A在何时释放GIL呢?如果等到A使用完了解释器之后才释放GIL,这也就意味着,并行的计算退化为了串行的计算,要这样的多线程机制有什么意义呢?所有毫无疑问的,Python拥有一套线程的调度机制。对于线程调度机制而言,同操作系统的进程调度一样,最关键的是要解决两个问题: 在何时挂起当前线程,选择处于等待状态的下一个线程? 在众多的处于等待状态的候选线程中,选择激活哪一个线程?在Python的多线程机制中,这两个问题是分别由不同的层 次解决的。对于何时进行线程调度的问题,是由Python自身决定的。考虑一下操作系统是如何进行进程的切换的,当一个进程执行了一段时间之后,发生了时 钟中断,操作系统响应时钟中断,并在这时开始进行进程的调度。同样,Python中也是通过软件模拟了这样的时钟中断,来激活线程的调度。我们知 道,Python字节码解释器的工作原理是按照指令的顺序一条一条地顺序执行,Python内部维护着一个数值,这个数值就是Python内部的时钟,如 果这个数值为N,则意味着Python在执行了N条指令以后应该立即启动线程调度机制在当前的2.5 中,Python的默认行为是在执行了100条指令以后启动线程调度机制。实际上,这个值不仅仅是用来进行线程调度的,在内部,Python也使用它来检 查是否有异步的事件(event)发生,需要处理。我们可以通过sys.setc- heckinterval()来调节这个值。现在我们知道了,Python控制着什么时候进行线程调度, 当一个线程获得了访问Python解释器的所必须的GIL并进入Python解释器后,Python内部的监测机制就开始启动,当这个线程执行了100条 指令之后,Python解释器将强制挂起当前线程,开始切换到下一个处于等待状态的进程。那么究竟Python会在众多的等待线程中选择哪一个幸运儿 呢?答案是,不知道。没错,对于这个问题,Python完全没有插手,而是交给了底层的操作系统来解决。也就是说,Python借用了底层操作系统所提供 的线程调度机制来决定下一个进入Python解释器的线程究竟是谁。这一点至关重要,这就意味着Python中的线程实际上就是 操作系统所支持的原生线程,而并非如坊间所流传的那样:Python的线程并非原生线程,而是模拟出来的。Python中的多线程机制正是建立在操作系统 的原生线程的基础之上,对应不同的操作系统,有不同的实现,然而最终,在各不相同的原生线程的基础之上,Python提供了一套统一的抽象机制,给 Python的使用者一个非常简单而方便的多线程工具箱,这就是Python中的两个module:thread以及在其之上的threading。15.2 初见Python ThreadPython所提供的最基础的多线程机制的接口是 thread module。这个module是一个builtin module,用C实现。在thread module的基础上,Python提供了一个更高层的多线程机制接口,即threding module。threading module是一个标准库中的module,用Python语言实现,为用户提供了更方便的多线程机制接口。我们的目标是要剖析Python中的多线程机制是如何实现的,而非学习在Python中如何进行多线程编程,所以重点会放在thread module上。通过这个module,看一看Python对操作系统的原生线程机制所做的精巧的包装。我们通过下面所示的thread1.py开始充满趣味的多线程之旅。thread1.pyimport threadimport timedef threadProc(): print sub thread id : , thread.get_ident() while True: print Hello from sub thread , thread.get_ident() time.sleep(1)print main thread id : , thread.get_ident()thread.start_new_thread(threadProc, ()while True: print Hello from main thread , thread.get_ident() time.sleep(1)在thread module中,Python向用户提供的多线程机制的接口其实可以说少得可怜,当然,也正因为如此,才使Python中的多线程编程变得非常的简单而方 便。我们来看看在thread module的实现文件threadmodule.c中,thread module为Python使用者提供的所有多线程机制接口。threadmodule.cstatic PyMethodDef thread_methods = start_new_thread, (PyCFunction)thread_PyThread_start_new_thread, start_new, (PyCFunction)thread_PyThread_start_new_thread, , allocate_lock, (PyCFunction)thread_PyThread_allocate_lock, , allocate, (PyCFunction)thread_PyThread_allocate_lock, , exit_thread, (PyCFunction)thread_PyThread_exit_thread, , exit, (PyCFunction)thread_PyThread_exit_thread, , interrupt_main, (PyCFunction)thread_PyThread_interrupt_main, get_ident, (PyCFunction)thread_get_ident, , stack_size, (PyCFunction)thread_stack_size, , NULL, NULL /* sentinel */;我们发现,thread module中有的接口居然以不同的形式出现了两次,比如“start_new_thread”和“start_new”,实际上在Python内部,对应的都是thread_ PyThread_start_new_thread这个函数。所以,thread module所提供的接口,真的是少得可怜。在我们的thread1.py中我们使用了其中两个接口。关于这两个接口的详细介绍,请参阅Python文档。15.3 Python线程的创建在Python的thread module所提供的接口中,一定不能少的肯定是创建线程的接口,倘若没有这个接口,生活还有什么意义呢J?在上面的thread1.py中,我们正是通 过其提供的start_new_thread创建了一个崭新的线程。好,我们就进入这个start_new_ thread,看看Python是如何进行创世纪的工作的(见代码清单15-1)。代码清单15-1threadmodule.cstatic 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-func = func; boot-args = args; boot-keyw = keyw; /2:初始化多线程环境 PyEval_InitThreads(); /* Start the interpreters thread-awareness */ /3:创建线程 ident = PyThread_start_new_thread(t_bootstrap, (void*) boot); return PyInt_FromLong(ident);在thread_PyThread_start_new_thread中,Python虚拟机通过三个主要的动作,完成一个线程的创建。代码清单15-1的1、2和3分别有如下含义:1 创建并初始化bootstate结构boot,在boot中,将保存关于线程的一切信息,如:线程过程,线程过程的参数等。2 初始化Python的多线程环境。3 以boot为参数,创建操作系统的原生线程。在代码清单15-1的1中,我们注意到 boot-interp中保存了Python的PyInter- preterState对象,这个对象中携带了Python的module pool这样的全局信息,Python中所有的thread都会共享这些全局信息。关于代码清单15-1的2处所示的多线程环境的初始化动 作,有一点需要特别说明,当Python启动时,是并不支持多线程的。换句话说,Python中支持多线程的数据结构以及GIL都是没有创建 的,Python之所以有这种行为是因为大多数的Python程序都不需要多线程的支持。假如一个简单地统计词频的Python脚本中居然出现了多线程, 面对这样的代码,我们一定都会抓狂的J。对多线程的支持并非是没有代价的,最简单的一点,如果激活多 线程机制,而执行的Python程序中并没有多线程,那么在100条指令之后,Python虚拟机同样会激活线程的调度。而如果不激活多线 程,Python虚拟机则不用做这些无用功。所以Python选择了让用户激活多线程机制的策略。在Python虚拟机启动时,多线程机制并没有被激活, 它只支持单线程,一旦用户调用thread.start_new_thread,明确指示Python虚拟机创建新的线程,Python就能意识到用户需 要多线程的支持,这个时候,Python虚拟机会自动建立多线程机制需要的数据结构、环境以及那个至关重要的GIL。15.3.1 建立多线程环境多线程环境的建立,说得直白一点,主要就是创建GIL。我们已经知道GIL对于Python的多线程机制的重要意义,然而这个GIL到底是如何实现的呢,呃,这是一个很有趣的问题。pythread.htypedef void *PyThread_type_lock;ceval.cstatic PyThread_type_lock interpreter_lock = 0; /* This is the GIL */static long main_thread = 0;void PyEval_InitThreads(void) if (interpreter_lock) return; interpreter_lock = PyThread_allocate_lock(); PyThread_acquire_lock(interpreter_lock, 1); main_thread = PyThread_get_thread_ident();终于见识到了神秘的GIL(interpreter_lock),没想到吧,万万没想到,它居然指示一个简单的void*。但是转念一想,在C中void*几乎可以是任何东西,这家伙,可是个万能容器啊。可以看到,无论 创建多少个线程,Python建立多线程环境的动作只会执行一次。在PyEval_InitThreads的开始,Python会检查GIL是否已经被创 建,如果是,则不再进行任何动作,否则,就会创建这个GIL。创建GIL的工作由PyThread_allocate_lock完成,我们来看一看这个 GIL到底是何方神圣。thread_nt.hPyThread_type_lock PyThread_allocate_lock(void) PNRMUTEX aLock; if (!initialized) PyThread_init_thread(); aLock = AllocNonRecursiveMutex() ; return (PyThread_type_lock) aLock;在这里,我们终 于看到了Python中多线程机制的平台相关性,在Python25Python目录下,有一大批thread_*.h这样的文件,在这些文件中, 包装了不同操作系统的原生线程,并通过统一的接口暴露给Python,比如这里的PyThread_allocate_lock就是这样一个接口。我们这 里的thread_nt.h中包装的是Win32平台的原生thread,在本章中后面的代码剖析中,还会有大量与平台相关的代码,我们都以Win32平 台为例。在 PyThread_allocate_lock中,与PyEval_InitThreads非常类似的,它会检查一个initialized的变量,如果 说GIL指示着Python的多线程环境是否已经建立,那么这个initialized变量就指示着为了使用底层平台所提供的原生thread,必须的初 始化动作是否完成。这些必须的初始化动作通常都是底层操作系统所提供的API,不同的操作系统可能需要不同的初始化动作。在Win32平台下,不需要任何 的初始化动作,所以PyThread_init_thread的唯一作用就是设置initialized变量:thread.cvoid PyThread_init_thread(void) if (initialized) return; initialized = 1; PyThread_init_thread();thread_nt.hstatic void PyThread_init_thread(void) 在 PyThread_allocate_lock中,出现了一个关键的结构体PNRMUTEX,我们发现,这个结构体是函数的返回值,实际上也就是 PyEval_InitThread中需要创建的那个interperter_lock(GIL)。原来GIL就是这个家伙,我们来看一看它的真身。thread_nt.htypedef struct NRMUTEX LONG owned ; DWORD thread_id ; HANDLE hevent ; NRMUTEX, *PNRMUTEX ;在NRMUTEX中,所有的数据成员的类型都是Win32平台下的类型风格了,owned和thread_id都很普通,而其中的HANDLE hevent却值得注意,我们来看看AllocNon- RecursiveMutex究竟为这个hevent准备了什么。thread_nt.hPNRMUTEX AllocNonRecursiveMutex(void) PNRMUTEX mutex = (PNRMUTEX)malloc(sizeof(NRMUTEX) ; if(mutex & !InitializeNonRecursiveMutex(mutex) free(mutex); Mutex = NULL; return mutex ;BOOL InitializeNonRecursiveMutex(PNRMUTEX mutex) mutex-owned = -1 ; /* No threads have entered NonRecursiveMutex */ mutex-thread_id = 0 ; mutex-hevent = CreateEvent(NULL, FALSE, FALSE, NULL) ; return mutex-hevent != NULL ; /* TRUE if the mutex is created */一切真相大白了,原来,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。thread_nt.hint PyThread_acquire_lock(PyThread_type_lock aLock, int waitflag) int success ; success = aLock & EnterNonRecursiveMutex(PNRMUTEX) aLock, (waitflag = 1 ? INFINITE : 0) = WAIT_OBJECT_0 ; return success;DWORD EnterNonRecursiveMutex(PNRMUTEX mutex, BOOL wait) /* Assume that the thread waits successfully */ DWORD ret; /* InterlockedIncrement(&mutex-owned) = 0 means that no thread currently owns the mutex */ if (!wait) if (InterlockedCompareExchange(PVOID *)&mutex-owned, (PVOID)0, (PVOID)-1) != (PVOID)-1) return WAIT_TIMEOUT ; ret = WAIT_OBJECT_0 ; else ret = InterlockedIncrement(&mutex-owned) ? /* Some thread owns the mutex, lets wait. */ WaitForSingleObject(mutex-hevent, INFINITE) : WAIT_OBJECT_0 ; mutex-thread_id = GetCurrentThreadId() ; /* We own it */ return ret ;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的系统APIInterlocked- 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的占有。thread_nt.hvoid PyThread_release_lock(PyThread_type_lock aLock) LeaveNonRecursiveMutex(PNRMUTEX) aLock);BOOL LeaveNonRecursiveMutex(PNRMUTEX mutex) /* We dont own the mutex */ mutex-thread_id = 0 ; return InterlockedDecrement(&mutex-owned) hevent) ; /* Other threads are waiting, wake one on them up */最终,一个线程在释放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主线程。最后,我们在图15-4中给出整个PyEval_InitThread的函数调用关系。图15-4 PyEval_InitThreads中的函数调用关系15.3.2 创建线程15.3.2.1 子线程的诞生在完成了多线程环境的初始化之后,Python会开始创建底 层平台的原生thread,以thread1.py为例,这个原生thread将执行threadProc所定义的操作。从现在开始,为了描述的清晰性, 我们将Python主线程,也就是调用thread_PyThread_start_new_thread创建新的线程的线程称为主线程,而将与 threadProc对应的原生thread称之为子线程。现在我们来看看一个子线程是如何被创建的(见代码清单15-2)。代码清单15-2threadmodule.cstatic 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-func = func; boot-args = args; boot-keyw = keyw; /2:初始化多线程环境 PyEval_InitThreads(); /* Start the interpreters 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 platforms default stack size or the size specified by the THREAD_STACK_SIZE macro. */static size_t _pythread_stacksize = 0;thread_nt.hlong 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;主线程通过调用 PyThread_start_new_thread完成创建子线程的工作。为了清晰地理解PyThread_start_new_thread的工作, 我们需要特别注意该函数的参数,从thread_ PyThread_start_new_thread中可以看到,这里的func实际上是函数t_bootstrap,而arg则是在 thread_PyThread_start_new_thread中创建的bootstate结构体boot。在boot中,保存着Python程序 (thread1.py)中所定义的线程的信息。PyThread_start_new_thread首先会将func和arg都打包到一个类型为callobj的结构体obj中,我们来看看这个obj。thread_nt.htypedef struct void (*func)(void*); void *arg; long id; HANDLE done; callobj;值得注意的是,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世界的游戏规则,谁也不能例外。所以,为了避免死锁,子线程一定会在申请GIL之前通知 obj.done。现在,是时候开始进入子线程,也就是那个bootstrap函数了,看看它究竟什么时候开始通知obj.done。thread_nt.hstatic intbootstrap(void *call) callobj *obj = (callobj*)call; /* copy callobj since other thread might free it before were done */ /这里将得到函数t_bootstrap void (*func)(void*) = obj-func; void *arg = obj-arg; obj-id = PyThread_get_thread_ident(); ReleaseSemaphore(obj-done, 1, NULL); func(arg); return 0;在bootstrap中,子线程完成了三个动作: 获得线程id; 通知obj-done内核对象; 调用t_bootstrap;在这里我们看到,子线程在申请GIL之前确实通知了前面提到 的obj.done内核对象以唤醒主线程。那么,Python为什么需要让主线程等待子线程的通知呢,在这里一切都明了了。原来,主线程所调用的 PyThread_start_new_thread需要返回所创建的子线程的线程id,然而子线程的线程id只有在子线程被激活后才能在子线程中获取, 因此主线程等的就是这个子线程id,一旦子线程设置了obj-id,就会设法唤醒主线程。从这里开始,主线程和子线程开始分道扬镳,主线程在返回子线程id之后,会继续执行后续的字节码,因为我们知道,这时候,主线程手里握着GIL。而子线程则将进入t_bootstrap,并最终进入等待GIL的状态。threadmodule.cstatic void t_bootstrap(void *boot_raw) struct bootstate *boot = (struct bootstate *) boot_raw; PyThreadState *tstate; PyObject *res; tstate = PyThreadState_New(boot-interp); PyEval_AcquireThread(tstate); res = PyEval_CallObjectWithKeywords(boot-func, boot-args, boot-keyw); PyMem_DEL(boot_raw); PyThreadState_Clear(tstate); PyThreadState_DeleteCurrent(); PyThread_exit_thread();子线程从这里开始了与主线程对GIL的竞争。在 t_bootstrap中,所进行的第一个动作PyEval_AcquireThread就是申请GIL,当 PyEval_AcquireThread结束之后,子线程也就获得了GIL,并且做好了一切执行的准备。接下来子线程通过PyEval_ CallObjectWithKeywords,将最终调用我们已经非常熟悉的PyEval_EvalFrameEx,也就是Python的字节码执行引 擎。传递进PyEval_CallObjectWithKeywords的boot-func是一个PyFunctionObject对象,正是 therad1.py中定义的threadProc编译后的结果。在PyEval_CallObjectWithKeywords结束之后,子线程将释放GIL,并完成销毁线程的所有扫尾工作,到了这里,子线程就结束了。从t_bootstrap的代码看上去,似乎子线程会一直执 行,直到子线程的所有计算都完成,

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论