关于Python GIL

线程

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

未完待续……

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容