77

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

如果 GIL 不允许并行执行多条指令,难道共享数据就不需要保护了吗?

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

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

4

9 回答 9

72

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

为了使更新保持一致,run 方法需要锁定 read-modify-write 部分(在循环内)周围的共享状态,或者有一些方法来检测共享状态自读取后何时发生变化

于 2008-09-19T20:34:40.610 回答
24

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

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

于 2008-09-19T20:13:54.920 回答
11

添加到讨论:

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

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

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

于 2008-10-28T00:16:12.273 回答
9

这篇文章在相当高的层次上描述了 GIL:

这些引语特别有趣:

每十条指令(此默认值可以更改),核心释放当前线程的 GIL。此时,操作系统从所有竞争锁的线程中选择一个线程(可能选择刚刚释放 GIL 的同一个线程——您无法控制选择哪个线程);该线程获取 GIL,然后再运行十个字节码。

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

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

于 2008-09-19T20:16:33.963 回答
8

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

我一次又一次遇到的答案是,Python 中的多线程很少值得开销,正因为如此。我听说过PyProcessing项目的好消息,它使运行多个进程像多线程一样“简单”,具有共享的数据结构、队列等。(PyProcessing 将作为多处理模块引入即将推出的 Python 2.6 的标准库中.) 这可以让您了解 GIL,因为每个进程都有自己的解释器。

于 2008-09-19T20:24:50.620 回答
4

这样想:

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

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

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

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

b = 5

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

所以你可以看到,即使它们实际上没有同时运行,你仍然需要锁定。

于 2008-12-22T09:49:35.747 回答
1

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

于 2008-09-19T20:11:18.843 回答
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 的回答通过代码示例进行了解释。
于 2013-12-05T19:01:46.660 回答
0

来自 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真的在任何时候只允许一个线程(时间切片线程),那么陈旧的值就不应该存在,对吧?

于 2008-12-22T02:56:47.000 回答