线程
Python线程并不是某种特有的高级抽象,而是基于操作系统线程的封装,比如在Linux上就是pthreads
的封装。Python线程的调度和管理并没有用自有算法,完全由操作系统控制。
每个Python线程都带有一个用于标志线程状态的PyThreadState
,参考Include/pystate.h。
同时Python/pystate.c定义了一个_PyThreadState_Current指针,指向当前运行线程的PyThreadState
。
GIL实现
static PyThread_type_lock interpreter_lock = 0; /* This is the GIL */
static PyThread_type_lock pending_lock = 0; /* for pending calls */
static long main_thread = 0;
int
PyEval_ThreadsInitialized(void)
{
return interpreter_lock != 0;
}
void
PyEval_InitThreads(void)
{
if (interpreter_lock) // 判断是否已有GIL
return;
interpreter_lock = PyThread_allocate_lock();
PyThread_acquire_lock(interpreter_lock, 1);
main_thread = PyThread_get_thread_ident();
}
其中锁interpreter_lock
的实现,由Python/thread_pthread.h可见其实是一个*sem_t
。
PyThread_type_lock
PyThread_allocate_lock(void)
{
sem_t *lock;
int status, error = 0;
dprintf(("PyThread_allocate_lock called\n"));
if (!initialized)
PyThread_init_thread();
lock = (sem_t *)malloc(sizeof(sem_t));
if (lock) {
status = sem_init(lock,0,1);
CHECK_STATUS("sem_init");
if (error) {
free((void *)lock);
lock = NULL;
}
}
dprintf(("PyThread_allocate_lock() -> %p\n", lock));
return (PyThread_type_lock)lock;
}
协作式多任务
一般对应于I/O密集型任务,当某个I/O任务需要等待一段不确定时间时(比如阻塞时),将主动释放GIL。以简单的一段服务器端代码为例:
import socket
def accept():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('localhost', 8001))
s.listen(1)
conn, addr = s.accept()
此处的accept
将会造成阻塞,其对应的实现可参考Modules/socketmodule.c,下面是其中的1743行到1747行:
Py_BEGIN_ALLOW_THREADS
timeout = internal_select_ex(s, 0, interval);
if (!timeout)
newfd = accept(s->sock_fd, SAS2SA(&addrbuf), &addrlen);
Py_END_ALLOW_THREADS
此处的Py_BEGIN_ALLOW_THREADS
宏实际执行了一次PyThread_release_lock
,即释放GIL,相应的Py_END_ALLOW_THREADS
执行了PyThread_acquire_lock
。
可见Python在可能造成阻塞的任务前会主动释放一次GIL,待可能造成阻塞的任务结束之后,再尝试获取GIL。
另,如果执行dis.dis(accept)
,将会得到下面的结果:
3 0 LOAD_GLOBAL 0 (socket)
3 LOAD_ATTR 0 (socket)
6 LOAD_GLOBAL 0 (socket)
9 LOAD_ATTR 1 (AF_INET)
12 LOAD_GLOBAL 0 (socket)
15 LOAD_ATTR 2 (SOCK_STREAM)
18 CALL_FUNCTION 2
21 STORE_FAST 0 (s)
4 24 LOAD_FAST 0 (s)
27 LOAD_ATTR 3 (bind)
30 LOAD_CONST 4 (('localhost', 8001))
33 CALL_FUNCTION 1
36 POP_TOP
5 37 LOAD_FAST 0 (s)
40 LOAD_ATTR 4 (listen)
43 LOAD_CONST 3 (1)
46 CALL_FUNCTION 1
49 POP_TOP
6 50 LOAD_FAST 0 (s)
53 LOAD_ATTR 5 (accept)
56 CALL_FUNCTION 0
59 UNPACK_SEQUENCE 2
62 STORE_FAST 1 (conn)
65 STORE_FAST 2 (addr)
68 LOAD_CONST 0 (None)
71 RETURN_VALUE
此处值得注意的是,在CALL_FUNCTION
这个字节码对应的函数实现中如上所述完成了一次释放/获取GIL的操作,也就是说GIL可以实现单个字节码范围内的原子性,但不保证实现单个字节码内的原子性。
抢占式多任务
一般对应CPU密集型任务。Python/ceval.c可见如下代码,每隔sys.getcheckinterval()
(默认100)个tick,当前运行的线程会主动释放GIL。
if (--_Py_Ticker < 0) {
if (*next_instr == SETUP_FINALLY) {
/* Make the last opcode before
a try: finally: block uninterruptible. */
goto fast_next_opcode;
}
_Py_Ticker = _Py_CheckInterval;
...
#ifdef WITH_THREAD
if (interpreter_lock) {
/* Give another thread a chance */
if (PyThreadState_Swap(NULL) != tstate)
Py_FatalError("ceval: tstate mix-up");
PyThread_release_lock(interpreter_lock);
/* Other threads may run now */
PyThread_acquire_lock(interpreter_lock, 1);
...
}
#endif
}
注意这里的tick并不是基于时间的,而是基于代码的,一般可以理解成一个语句(此处存疑,是一个语句还是一个bytecode?)为一个tick,同时注意单个tick是不能被包括Ctrl+C
在内的方法中断的。
延伸话题,为什么Python线程经常不能被Ctrl+C
这样的信号中断?因为GIL的影响,如果当前有个耗时长的tick在运行,那么signal handler是无法捕捉到信号的。
偶尔还会发生这种情况,即Ctrl+C无法中断其他子线程正在执行的任务。这是因为signal handler只在主线程中运行,尽管信号到达解释器端后检查间隔由100变成1(由于check次数大大增加,程序会变得更慢),但由于等待获取GIL的线程太多,主线程也不能及时获得GIL,让signal handler处理中断。
线程调度
开头提过,Python解释器不控制进程调度,自然也不会有任何选举算法之类的东西,它能做的就是尽快地切换线程(不论业务是否需要),而操作系统对Python层面的线程任务一无所知,这是导致很多时候Python的线程切换看上去很傻的根本原因。
未完待续……
网友评论