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
绝对是一个 dict
和 word
绝对是一个 str
并且文档中保证所有操作(或者,如果没有,通过阅读 C 源代码)来保存 GIL,因此您可以确定 word in counts
是原子的。
嗯,可以确定是in CPython3.7;如果您的代码必须在 3.5 或 2.7 上 运行,您也必须检查那里。如果它必须在 Jython 上 运行,Jython 甚至没有 GIL……
此外,您很少需要首先对线程内部循环内的代码进行微优化,因为这意味着您的代码是 CPU-bound,在这种情况下您可能应该'我一开始就没有使用线程和共享变量。
如果 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
绝对是一个 dict
和 word
绝对是一个 str
并且文档中保证所有操作(或者,如果没有,通过阅读 C 源代码)来保存 GIL,因此您可以确定 word in counts
是原子的。
嗯,可以确定是in CPython3.7;如果您的代码必须在 3.5 或 2.7 上 运行,您也必须检查那里。如果它必须在 Jython 上 运行,Jython 甚至没有 GIL……
此外,您很少需要首先对线程内部循环内的代码进行微优化,因为这意味着您的代码是 CPU-bound,在这种情况下您可能应该'我一开始就没有使用线程和共享变量。