美文网首页
关于Python GIL

关于Python GIL

作者: 521851ef | 来源:发表于2018-07-22 00:24 被阅读0次

    线程

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

    未完待续……

    相关文章

      网友评论

          本文标题:关于Python GIL

          本文链接:https://www.haomeiwen.com/subject/qznkmftx.html