如果您使用的是实现了全局解释器锁(例如CPython)并编写多线程代码的Python,那么您真的需要锁吗?
如果GIL不允许多条指令并行执行,那么共享数据是否就不必要进行保护了呢?
如果这是一个愚蠢的问题,我很抱歉,但我一直想知道在多处理器/核心机器上运行Python是否也一样。
对于任何其他实现了GIL的语言实现,同样适用。
如果您使用的是实现了全局解释器锁(例如CPython)并编写多线程代码的Python,那么您真的需要锁吗?
如果GIL不允许多条指令并行执行,那么共享数据是否就不必要进行保护了呢?
如果这是一个愚蠢的问题,我很抱歉,但我一直想知道在多处理器/核心机器上运行Python是否也一样。
对于任何其他实现了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
)之间被中断,导致丢失更新。结果是共享状态的随机值。shared_balance += 100
和 shared_balance -= 100
,这样安全吗? - mrgloom补充讨论:
由于全局解释器锁(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中。 - Peter Hansen这样想:
在单处理器计算机上,多线程是通过暂停一个线程并启动另一个线程来实现的,速度足够快,使其看起来像是同时运行。这就像GIL下的Python:只有一个线程实际上在运行。
问题在于,线程可以在任何地方被暂停,例如,如果我想计算b =(a + b)* 3,这可能会产生类似以下的指令:
1 a += b
2 a *= 3
3 b = a
现在,假设程序运行在一个线程中,该线程在第一行或第二行被挂起,然后另一个线程启动并运行:
b = 5
那么当另一个线程恢复时,变量 b 会被旧的计算值覆盖,这可能不是预期的结果。
因此,即使它们实际上没有同时运行,你仍然需要加锁。
锁仍然是必需的。我将尝试解释为什么它们是必需的。
任何操作/指令都是在解释器中执行的。GIL确保解释器在特定时间被单个线程持有。而您的具有多个线程的程序在单个解释器中工作。在任何特定时间,该解释器只由一个线程持有。这意味着只有持有解释器的线程在任何时刻运行。
假设有两个线程,比如t1和t2,两者都想要执行读取全局变量值并将其增加的两个指令。
#increment value
global var
read_var = var
var = read_var + 1
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。你仍然需要使用锁定(因为你的代码可能随时被中断以执行另一个线程,这可能会导致数据不一致)。 GIL 的问题在于它防止 Python 代码同时使用更多核心(或者如果有多个处理器可用,则无法使用它们)。
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