Python 多线程中的锁和 GIL

Python Lock and GIL in Multithreading

如果 Python 有 GIL,甚至多线程程序也不会 运行 并发,除非它们是 I/O 绑定,我们真的需要 Python 中的 Lock 概念吗?

简单的答案是,您需要在共享可变数据上的每个操作周围加锁,而“操作”对您的算法的意义可能比 GIL 保护的要大得多。


用具体的例子比用抽象的例子更容易理解事物,所以让我们想一个。你有一个可迭代的行,你想计算字数。对于每一行,您调用此函数:

def updatecounts(counts, line):
    for word in line.split():
        if word in counts:
            counts[word] += 1
        else:
             counts[word] = 1

现在,您不再只是在循环中调用 updatecounts,而是创建一个线程执行器或池并调用 pool.map(partial(updatecounts, count), lines)。 (好的,这很愚蠢,但是假设您有 100 个客户端套接字生产线;那么让线程在其他工作中调用此函数是有意义的。)

线程 1 正在处理以“Now”开头的第 1 行。它检查 ”Now” 是否在 counts 中。不是。所以……但是随后线程被中断,线程 3 接管了。它的行也以“Now”开头,因此它检查 ”Now” 是否在 counts 中。不,所以它将 counts["Now"] 设置为 1。然后它移动到下一个单词并且......在某个时候,线程 1 再次开始执行。它要做什么?它将 counts["Now"] 设置为 1。我们刚刚丢失了一个计数。


防止这种情况的方法是传递一个锁:

def 更新计数(计数、计数锁、行): 对于 line.split() 中的单词: 带计数锁: 如果重要的话: 计数[单词] += 1 别的: 计数[单词] = 1

现在,如果线程 1 在检查 if word in counts: 后被中断,它仍然持有 countslock。所以当线程 3 试图获取相同的 countslock 时,它不能;它阻塞直到锁被释放。系统可能会 运行 一些其他线程一段时间,但最终它保证会回到线程 1,以便它可以完成它的工作并释放锁,然后线程 3 可以做任何事情。


为什么 GIL 不在这里保护我们?因为 GIL 不知道您要保护整四行。

如果我们只使用 Counter,那么我们可以写成 counts[word] += 1 会怎么样?好吧,那可能只是一行源代码,但它仍然编译成多个字节码,而GIL实际保护的级别是字节码。

事实上,从您的代码的角度来看,“字节码”是什么并不明显。你可以在 dis 模块的帮助下解决这个问题,但即使那样也并不总是很清楚。例如,words in count 被编译为一个字节码来进行比较——除了字节码实际上调用了 counts 上的 __contains__ 方法。 CPython 在 C 中实现 dict.__contains__ 而不是 Python,并且不发布 GIL。但是,如果 counts 可能是在 Python 中实现的某种映射(如 Counter),它需要多个字节码来实现该方法怎么办?或者,即使使用字典,__contains__(word) 最终也必须调用 word.__hash__。那可以释放 GIL 吗?


偶尔,当你真的需要优化一些内部循环时,值得做所有的工作来验证 counts 绝对是一个 dictword 绝对是一个 str 并且文档中保证所有操作(或者,如果没有,通过阅读 C 源代码)来保存 GIL,因此您可以确定 word in counts 是原子的。

嗯,可以确定是in CPython3.7;如果您的代码必须在 3.5 或 2.7 上 运行,您也必须检查那里。如果它必须在 Jython 上 运行,Jython 甚至没有 GIL……

此外,您很少需要首先对线程内部循环内的代码进行微优化,因为这意味着您的代码是 CPU-bound,在这种情况下您可能应该'我一开始就没有使用线程和共享变量。