python-源码阅读(5)-python线程

GIL

1
2
3
全局解释器锁(Global Interpreter Lock) 当CPython创建变量时,它会分配内存然后计算对
该变量的引用数量,大家通常称之为“引用计数”。 如果引用计数变为0,则从系统中释放该内存。引用 计数变量时需要保护竞争条件,多个线程同时增加 或减少变量引用计数时,可能导致内存泄漏或者错 误的内存释放。 CPython引入了GIL,线程在执行代码时,必须首先 获得解释器的使用权,虽然保证了数据安全,也意 味着单进程下Python多线程的性能没有那么好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[ceval_gil.h]
static _Py_atomic_int gil_locked = {-1};
static _Py_atomic_address gil_last_holder = {0};
static COND_T gil_cond;
static MUTEX_T gil_mutex;

static void create_gil(void)
{
MUTEX_INIT(gil_mutex);
MUTEX_INIT(switch_mutex);
COND_INIT(gil_cond);
COND_INIT(switch_cond);
_Py_atomic_store_relaxed(&gil_last_holder, 0);
_Py_ANNOTATE_RWLOCK_CREATE(&gil_locked);
_Py_atomic_store_explicit(&gil_locked, 0, _Py_memory_order_release);
}


[ceval_gil.h]
static void take_gil(PyThreadState *tstate)
{
int err;
err = errno;
MUTEX_LOCK(gil_mutex);
if (!_Py_atomic_load_relaxed(&gil_locked))
goto _ready;

while (_Py_atomic_load_relaxed(&gil_locked)) {
int timed_out = 0;
COND_TIMED_WAIT(gil_cond, gil_mutex, INTERVAL, timed_out);
...
}
_ready:
...
_Py_atomic_store_relaxed(&gil_locked, 1);
...
}

线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[Modules/_threadmodule.c]
static PyMethodDef thread_methods[] = {
{"start_new_thread", (PyCFunction)thread_PyThread_start_new_thread,
METH_VARARGS, start_new_doc},
{"start_new", (PyCFunction)thread_PyThread_start_new_thread,
METH_VARARGS, start_new_doc},
{"allocate_lock", (PyCFunction)thread_PyThread_allocate_lock,
METH_NOARGS, allocate_doc},
{"allocate", (PyCFunction)thread_PyThread_allocate_lock,
METH_NOARGS, allocate_doc},
...
{NULL, NULL} /* sentinel */
};


[cveal.c]
static PyThread_type_lock pending_lock = 0; /* for pending calls */
static unsigned long main_thread = 0;
/* Request for dropping the GIL */
static _Py_atomic_int gil_drop_request = {0};

void PyEval_InitThreads(void)
{
if (gil_created()) // 如果初始化过了
return;
create_gil();
take_gil(PyThreadState_GET());
main_thread = PyThread_get_thread_ident();
if (!pending_lock)
pending_lock = PyThread_allocate_lock();
}

从GIL到字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[pystate.c]
PyThreadState * PyThreadState_New(PyInterpreterState *interp)
{
return new_threadstate(interp, 1);
}

static PyThreadState * new_threadstate(PyInterpreterState *interp, int init)
{
PyThreadState *tstate = (PyThreadState *)PyMem_RawMalloc(sizeof(PyThreadState));

if (_PyThreadState_GetFrame == NULL) // 设置获得线程中函数调用栈的操作
_PyThreadState_GetFrame = threadstate_getframe;

...
if (init)
_PyThreadState_Init(tstate);
...
return tstate;
}

void _PyThreadState_Init(PyThreadState *tstate)
{
#ifdef WITH_THREAD
_PyGILState_NoteThreadState(tstate);
#endif
}

子线程在创建了自身的线程状态对象后, 通过 _PyGILState_NoteThreadState 将这个线程对象设置到 autoTLSkey 中.

有意思的是, 这时候当前活动的python线程不一定是获得GIL的线程. 这是因为主线程和子线程都是操作系统原生线程, 所以操作系统可能在主线程和子线程之间切换. 这里要区分的一点是, 系统级别的线程调度与GIL无关. 而python级的线程调度才与GIL有关, 前面也说了, python自己模拟一套线程的调度方式. 所以操作系统级的并不一定意味着GIL的得手, 当所有的线程都完成了初始化的动作之后, 操作系统的线程调度和python的线程调度才会同一. 那时, python的线程调度会强制当前活动线程释放GIL, 而这一操作会触发Event内核对象, 这个触发进而触发操作系统进行线程调度. 这个GIL其实是起到了从python级线程到操作系统级线程调度的桥梁作用.

真正争夺GIL的是在 t_bootstrap 中, 透过 PyEval_AcquireThread 获得GIL的话语权. 到了这一步, 子线程将自己挂起, 操作系统的线程调度不能唤起它, 只有等待python的线程调度强制主线程放弃GIL后, 子线程才回被唤醒. 而子线程被唤醒后, 主线程又将陷入等待中.

当子线程被python的线程调度唤醒后, 做的第一件事就是通过 PyThreadState_Swap(tstate) 设置python维护当前线程状态对象, 如操作系统的进程上下文环境恢复一样.