大师兄的Python源码学习笔记(三十九): Python的多线程机制(一)

大师兄的Python源码学习笔记(三十八): 模块的动态加载机制(五)
大师兄的Python源码学习笔记(四十): Python的多线程机制(二)

一、GIL与线程调度

1. 线程互斥
  • 由于同一个CPU在同一时间只能处理一个线程,所以在处理多线程时CPU经常需要在不同线程间切换。
  • 我们知道在同一个进程内,不同线程是共享同一片内存的,所以可能会出现以下案例情况:
  • A、B两个线程同时保存着对内存中同一对象obj的引用。
Include\object.h

typedef struct _object {
   _PyObject_HEAD_EXTRA
   Py_ssize_t ob_refcnt;
   struct _typeobject *ob_type;
} PyObject;
  • 也就是说obj->ob_refcnt为2。
  • 如果A销毁对obj的引用,将通过Py_DECREF调整obj的引用计数器。
Include\object.h

#define Py_DECREF(op)                                   \
   do {                                                \
       PyObject *_py_decref_tmp = (PyObject *)(op);    \
       if (_Py_DEC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
       --(_py_decref_tmp)->ob_refcnt != 0)             \
           _Py_CHECK_REFCNT(_py_decref_tmp)            \
       else                                            \
           _Py_Dealloc(_py_decref_tmp);                \
   } while (0)
  • 如果在A执行完第一个动作后,也就是obj->ob_refcnt为1时,线程从A切换到B,并且B也执行销毁对obj的引用,也就是obj->ob_refcnt变为0,并将对象销毁,释放内存。
  • 当B再切换回A时,A开始再一次对已销毁的对象进行销毁和内存释放动作,将引起问题。
  • 为了避免上面案例的情况,Python使用GIL(Global Interpreter Lock,全局解释器锁)来实现不同线程对共享资源访问的互斥
2. 霸道的GIL
  • Python中的GIL是非常强势的存在,正如它的名字一般,他是虚拟机一级的互斥机制,是一个解释器(Interpreter)
  • 也就是说,在一个线程拥有了解释器的访问权限后,其他所有线程都必须等待他释放解释器的访问权,即是这些线程的下一条指令不会互相影响。
  • GIL的存在就意味着即使在多核CPU环境下,在同一时间,也只能有一个线程访问Python所提供的API。
  • 理论上,GIL的存在应该使Python性能大打折扣,所以在99年,Greg Stein和Mark Hammond曾经尝试了一份去除GIL的CPython branch,但效率只有原来的一半左右。
  • 目前,GIL虽然一直在被不断优化,但仍旧是工业实践的最优解。
3. GIL的线程调度机制
  • 在Python中,字节码解释器是核心存在,所以通过GIL来互斥不同线程对解释器的使用。
  • 在上图中,A、B和C都需要使用解释器来执行字节码,但在这之前,他们必须获得GIL
  • 实际上,GIL背后所保护的不仅仅是Python解释器,还有Python的C API,在C/C++和Python的混合开发中,在涉及到Python线程的协作时,也需要通过GIL互斥。
  • 当A获得GIL后,B、C只能等待A释放GIL后才能进入解释器。
  • 同CPU一样,GIL也有一套线程的调度机制,这套机制要解决两个关键问题:
3.1 何时挂起当前线程
  • 对于何时进行线程调度,是由Python自身决定的。
  • Python会通过软件模拟系统的时钟中断来激活线程的调度。
>>> import sys
>>> sys.getswitchinterval()
0.005
  • 这个0.005就是Python3.8默认的线程切换间隔时间。
  • 在Python内部,也使用它来检查是否有异步事件发生。
  • 可以通过sys.setswitchinterval()来调节这个值。
3.2 如果选择激活哪一个等待线程
  • 对于这个问题,Python完全没有插手,而是交给底层的操作系统来解决。
  • 也就是说,Python借用了底层操作系统所提供的的线程调度机制来决定下一个进入Python解释器线程究竟是谁。
  • 这意味着Python中的线程实际上就是操作系统所支持的原生线程,对应不同的操作系统有不同的实现。
  • 在原生线程的基础上,Python提供了一套统一的抽象机制和工具箱,就是_thread包和threading包。

你可能感兴趣的:(大师兄的Python源码学习笔记(三十九): Python的多线程机制(一))