关于Python GIL

2018-07-22  本文已影响0人  521851ef

线程

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的线程切换看上去很傻的根本原因。

未完待续……

上一篇 下一篇

猜你喜欢

热点阅读