如果您依赖具有全局解释器锁(即CPython)的Python实现并编写多线程代码,那么您真的需要锁吗?
如果GIL不允许并行执行多个指令,那么共享数据是否有必要保护吗?
抱歉,这是一个愚蠢的问题,但这是我一直想知道的关于多处理器/核心计算机上的Python的东西。
同样的情况也适用于具有GIL的任何其他语言实现。
Answers:
如果您在线程之间共享状态,则仍然需要锁。GIL仅在内部保护解释器。您自己的代码中仍然可能存在不一致的更新。
例如:
#!/usr/bin/env python
import threading
shared_balance = 0
class Deposit(threading.Thread):
def run(self):
for _ in xrange(1000000):
global shared_balance
balance = shared_balance
balance += 100
shared_balance = balance
class Withdraw(threading.Thread):
def run(self):
for _ in xrange(1000000):
global shared_balance
balance = shared_balance
balance -= 100
shared_balance = balance
threads = [Deposit(), Withdraw()]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print shared_balance
在这里,您的代码可能会在读取共享状态(balance = shared_balance
)和将更改后的结果写回(shared_balance = balance
)之间中断,从而导致更新丢失。结果是共享状态的随机值。
为了使更新一致,运行方法需要将共享状态锁定在read-modify-write部分周围(循环内部),或者需要某种方法来检测共享状态自被读取以来何时发生了变化。
shared_balance += 100
,会安全shared_balance -= 100
吗?
添加到讨论中:
由于存在GIL,因此某些操作在Python中是原子的,不需要锁。
http://www.python.org/doc/faq/library/#what-kinds-of-global-value-mutation-are-thread-safe
但是,如其他答案所述,无论何时应用程序逻辑需要锁,您仍需要使用锁(例如在生产者/消费者问题中)。
这篇文章从较高的层次描述了GIL:
这些引号特别引人注意:
每十个指令(可以更改此默认值),内核将释放当前线程的GIL。那时,操作系统从所有争夺锁的线程中选择一个线程(可能选择了刚刚释放GIL的相同线程–您无法控制选择哪个线程);该线程获取GIL,然后运行另外十个字节码。
和
请注意,GIL仅限制纯Python代码。可以编写扩展(释放通常是用C语言编写的外部Python库)来释放锁,然后允许Python解释器与扩展分开运行,直到扩展重新获取锁为止。
听起来,GIL只是为上下文切换提供了更少的可能实例,并使多核/处理器系统对于每个python解释器实例而言都表现为单个核,因此,是的,您仍然需要使用同步机制。
sys.getcheckinterval()
告诉您在“ GIL版本”之间执行了多少字节码指令(从2.5开始至少为100(而不是10))。在3.2中,它可能会切换到基于时间的间隔(大约5毫秒),而不是指令计数。该更改可能仍适用于2.7,尽管仍在进行中。
全局解释器锁定可防止线程同时访问解释器(因此CPython只能使用一个内核)。但是,据我了解,线程仍然被抢先中断和调度,这意味着您仍然需要在共享数据结构上加锁,以免线程踩到对方的脚趾。
我一次又一次遇到的答案是,因此,Python中的多线程很少值得开销。我听说过PyProcessing项目的好处,该项目使多个进程像共享线程一样,以多线程的方式“简单”运行。(PyProcessing将作为多处理模块引入即将到来的Python 2.6的标准库中。)这使您绕过GIL,因为每个进程都有自己的解释器。
这样想:
在单处理器计算机上,多线程是通过挂起一个线程并以足够快的速度启动另一个线程以使其看起来同时在运行而发生的。这就像带有GIL的Python:实际上只有一个线程在运行。
问题是线程可以挂在任何地方,例如,如果我要计算b =(a + b)* 3,则可能会产生类似以下的指令:
1 a += b
2 a *= 3
3 b = a
现在,假设该线程在一个线程中运行,并且该线程在第1行或第2行之后挂起,然后另一个线程进入并运行:
b = 5
然后,当另一个线程恢复时,b被旧的计算值覆盖,这可能不是预期的。
因此,您可以看到,即使它们实际上并没有同时运行,您仍然需要锁定。
仍然需要锁。我将尝试解释为什么需要它们。
任何操作/指令都在解释器中执行。GIL确保在特定时间由单个线程保留解释器。您的具有多个线程的程序可以在单个解释器中运行。在任何特定时间,此解释器都由单个线程持有。这意味着在任何时候都只有运行解释器的线程正在运行。
假设有两个线程,例如t1和t2,并且都想执行两条指令,它们正在读取全局变量的值并将其递增。
#increment value
global var
read_var = var
var = read_var + 1
如上所述,GIL仅确保两个线程不能同时执行一条指令,这意味着两个线程不能read_var = var
在任何特定时间执行。但是他们可以一个接一个地执行指令,您仍然会遇到问题。考虑这种情况:
read_var = var
。因此,t1中的read_var为0。GIL仅确保此读取操作不会在此刻对任何其他线程执行。read_var = var
。但是read_var仍然为0。因此,t2中的read_var为0。var = read_var+1
并且var变为1。var = read_var+1
并且var变为1。var
应该变成2。Will Harris的示例进行了一些更新:
class Withdraw(threading.Thread):
def run(self):
for _ in xrange(1000000):
global shared_balance
if shared_balance >= 100:
balance = shared_balance
balance -= 100
shared_balance = balance
在撤回中放置一个价值检查语句,我不再看到负值,并且更新似乎是一致的。我的问题是:
如果GIL阻止在任何原子时间只能执行一个线程,那么旧值在哪里?如果没有陈旧的价值,为什么我们需要锁定?(假设我们只谈论纯Python代码)
如果我理解正确,那么上述条件检查将无法在实际的线程环境中进行。当多个线程并发执行时,可以创建陈旧值,因此共享状态不一致,那么您确实需要一个锁。但是,如果python确实在任何时间都只允许一个线程(时间切片线程),那么应该就不可能存在过时的值,对吗?