由于GIL,多线程Python代码中是否需要锁定?

时间:2008-09-19 20:07:38

标签: python multithreading locking

如果您依赖于具有全局解释器锁(即CPython)并编写多线程代码的Python实现,那么您真的需要锁吗?

如果GIL不允许并行执行多条指令,那么共享数据是否不需要保护?

抱歉,如果这是一个愚蠢的问题,但我总是想知道多处理器/核心机器上的Python。

同样的事情适用于任何其他具有GIL的语言实现。

9 个答案:

答案 0 :(得分:68)

如果您在线程之间共享状态,则仍需要锁定。 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)之间中断,从而导致更新丢失。结果是共享状态的随机值。

要使更新保持一致,运行方法需要锁定读取 - 修改 - 写入部分(在循环内)的共享状态,或者使用some way to detect when the shared state had changed since it was read

答案 1 :(得分:22)

不 - GIL只是保护python内部的多个线程改变它们的状态。这是一个非常低级别的锁定,足以使python自己的结构保持一致状态。它不包括您需要做的应用程序级锁定,以便在您自己的代码中覆盖线程安全。

锁定的本质是确保代码的特定仅由一个线程执行。 GIL强制执行此操作以阻止单个字节码的大小,但通常您希望锁定跨越比此更大的代码块。

答案 2 :(得分:10)

加入讨论:

因为GIL存在,所以某些操作在Python中是原子的,不需要锁定。

http://www.python.org/doc/faq/library/#what-kinds-of-global-value-mutation-are-thread-safe

然而,正如其他答案所述,仍然需要在应用程序逻辑需要时使用锁(例如在生产者/消费者问题中)。

答案 3 :(得分:8)

这篇文章描述了相当高级别的GIL:

特别感兴趣的是这些引用:

  

每十条指令(默认为此   可以改变),核心发布   当前线程的GIL。在那   点,操作系统从中选择一个线程   所有线程都争夺锁定   (可能选择相同的线程   刚刚发布了GIL - 你没有   对任何线程有任何控制权   被选中);该线程获得了   GIL再跑10个   字节码。

  

请注意仅限GIL   限制纯Python代码。扩展   (通常是外部Python库   用C)写的可以写成   释放锁,然后允许   要运行的Python解释器   与延伸分开直到   扩展程序重新获取锁定。

听起来GIL只是为上下文切换提供了更少的可能实例,并且使得多核/处理器系统在每个python解释器实例方面都表现为单个核心,所以是的,你仍然需要使用同步机制

答案 4 :(得分:8)

全局解释器锁可以防止线程同时访问解释器(因此CPython只使用一个核心)。但是,据我所知,线程仍然被中断并预先抢先,这意味着您仍然需要锁定共享数据结构,以免线程踩到彼此的脚趾。

我一次又一次遇到的答案是Python中的多线程很少值得开销,因为这样。我听说过PyProcessing项目的优点,它使多个进程像多线程一样运行,具有共享数据结构,队列等等。(PyProcessing将被引入到即将发布的Python 2.6的标准库中)作为multiprocessing模块。)这可以让你绕过GIL,因为每个进程都有自己的解释器。

答案 5 :(得分:3)

这样想:

在单处理器计算机上,多线程通过挂起一个线程并以足够快的速度启动另一个线程以使其看起来同时运行来实现。这就像使用GIL的Python一样:实际上只有一个线程正在运行。

问题是线程可以在任何地方挂起,例如,如果我想计算b =(a + b)* 3,这可能产生如下所示的指令:

1    a += b
2    a *= 3
3    b = a

现在,假设它在一个线程中运行,并且该线程在第1行或第2行之后被挂起,然后另一个线程启动并运行:

b = 5

然后当另一个线程恢复时,b被旧的计算值覆盖,这可能不是预期的。

所以你可以看到即使它们不能同时运行,你仍然需要锁定。

答案 6 :(得分:1)

您仍然需要使用锁(您的代码可能随时被中断以执行另一个线程,这可能导致数据不一致)。 GIL的问题在于它可以防止Python代码同时使用更多内核(或多个处理器可用)。

答案 7 :(得分:1)

仍然需要锁。我会尝试解释为什么需要它们。

在解释器中执行任何操作/指令。 GIL确保解释器由在特定时刻的单个线程持有。并且具有多个线程的程序在单个解释器中工作。在任何特定时刻,该解释器由单个线程保持。这意味着在任何时刻只有持有解释器的线程正在运行

假设有两个线程,比如t1和t2,并且两个线程都想执行两条指令,这两条指令正在读取全局变量的值并递增它。

#increment value
global var
read_var = var
var = read_var + 1

如上所述,GIL仅确保两个线程不能同时执行指令,这意味着两个线程都不能在任何特定时刻执行read_var = var。但他们可以一个接一个地执行指令,你仍然可以遇到问题。考虑一下这种情况:

  • 假设read_var为0。
  • GIL由线程t1持有。
  • t1执行read_var = var。因此,t1中的read_var为0. GIL只会确保此时不会对任何其他线程执行此读操作。
  • GIL被赋予线程t2。
  • t2执行read_var = var。但read_var仍为0.因此,t2中的read_var为0。
  • GIL被赋予t1。
  • t1执行var = read_var+1,var变为1。
  • GIL被赋予t2。
  • t2认为read_var = 0,因为它读的是什么。
  • t2执行var = read_var+1,var变为1。
  • 我们的期望是var应该变为2。
  • 因此,必须使用锁来保持读取和递增作为原子操作。
  • Will Harris'答案通过代码示例解释。

答案 8 :(得分:0)

威尔哈里斯的例子中有一点更新:

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真的只允许任何时候只有一个线程(时间切片线程),那么就不可能存在过时的值,对吗?