Python 多线程

介绍python中的多线程。

真正的多线程吗?

对于多核处理器,在同一时间确实可以多个线程独立运行,但在Python中确不是这样的了。原因在于,python虚拟机中引入了GIL这一概念。GIL(Global Interpreter Lock)全局解析器锁是用来解决共享资源访问的互斥问题,导致在python虚拟机中同一时间只能有一个线程访问python所提供的API。

那么python是如何支持多线程的呢?

在操作系统中系统通过时钟中断进行进程的调度,而Python正是参考这个原理。

在python内部维护了一个内部的时钟,来记录每个线程每个时钟周期执行命令的数量。

1
2
3
>>> import sys
>>> sys.getcheckinterval() #获取一个始终周期内执行指令数
100

线程执行100条指令会发生线程切换。

当一个线程获得了Python虚拟机GIL后可以按顺序执行100条指令,然后挂起当前进程,切换下一个等待执行的线程。

那么Python如何选择下一个需要执行的线程呢?

Python并没有去实现一个线程优先级调度算法,而是将线程选择问题交给了底层的操作系统。

Python借用了底层操作系统所提供的线程调度机制来决定下一个执行的线程。

因此,Python使用的就是操作系统原生的线程,只是Python在其基础之上提供了一套统一的抽象机制。

线程切换

在操作系统中,进程之间的切换需要不断保存和恢复进程之间的上下文环境,保证每一个进行都能在其对应的上下文环境中运行。

Python正是参考操作系统的切换机制,为每一个线程创建一个保存线程状态信息的PyFrameObject对象。

在Python中有有一个全局变量PyThreadState *_PyThreadState_Current用来保存当前活动线程的线程状态对象。

下一线程切换需要的线程状态如何获取?

在Python中通过一个单项链表来管理所有的python线程对象(保护线程的状态信息和线程信息,例如线程id),当需要寻找一个线程对应的状态对象时,就遍历这个链表,搜索其对应的状态对象。

这个状态对象链表并不会受到GIL的保护,而是有其专用的锁。

需要注意

当前活动的Python线程不一定是获得了GIL的线程。

例如:

主线程获得了GIL,子线程还没有申请到GIL时也没有挂起,而且主线程和子线程都是操作系统原生的线程,操作系统可能在主线程和子线程之间进行切换。

操作系统的线程切换是不受python虚拟机控制的,属于操作系统自身行为。

Python虚拟机的调度是一定是获得GIL基础之上的,而操作系统级的线程是不一定获得GIL。

虽有操作系统会把未获得GIL的线程切换为活动线程,但是该线程发现自身并没有获得GIL会自动挂起。

只有当所有线程都完成了初始化操作,操作系统的线程调度和Python线程调度才会一致。

Python的线程调度会迫使当前活动线程释放GIL,导致触发GIL中维护的Event内核对象,从而触发操作系统的线程调度。

阻塞调度和线程销毁

在python中如果有raw_input等待输入的操作时将自身阻塞后,并将等待GIL线程唤醒,这种情况成为阻塞调度。

在线程通过阻塞调度切换时,python内部的时钟周期技术_Py_Ticker依然会被保持,不会被重置。

python的主线程销毁和子线程销毁是不同的,子线程只需要维护引用计数,而主线程还需要销毁运行环境。

用户级互斥和同步

GIL实现了保护内存的共享资源,而用户级互斥保护了用户程序中的共享资源

Python中提供了Lock机制来实现线程之间的互斥

当线程通过Lock.acquire获得Lock之后,子线程会因为等待Lock而挂起,直到主线程释放Lock之后才会被Python的线程调度机制唤醒。

在线程执行过程中如果出现需要等待另一个Lock资源的时候,需要将GIL转交给其他等待GIL的线程以避免死锁。