由于GIL,在多线程Python代码中是否不需要锁?
如果我们依赖具有全局解释器锁(即CPython)的Python实现并编写多线程代码,那么我们真的需要锁吗?
如果GIL不允许并行执行多个指令,那么共享数据是否有必要保护吗?
抱歉,如果这是一个愚蠢的问题,但这是我一直想知道的关于多处理器/核心计算机上的Python的东西。
同样的情况也适用于具有GIL的任何其他语言实现。
解决方案
我们仍然需要使用锁(代码可以随时中断以执行另一个线程,这可能导致数据不一致)。 GIL的问题在于,它阻止了Python代码同时使用更多的内核(或者多个处理器,如果可用的话)。
没有GIL会保护python内部免受多个线程更改其状态的影响。这是一个非常低级的锁定,仅足以使python自己的结构保持一致状态。它没有涵盖我们需要执行的应用程序级别锁定,以覆盖我们自己的代码中的线程安全性。
锁定的本质是确保特定代码块仅由一个线程执行。 GIL对单个字节码大小的块强制执行此操作,但是通常我们希望锁跨越比此更大的代码块。
这篇文章从较高的层次描述了GIL:
- https://web.archive.org/web/20080516010343/http://www.pyzine.com/Issue001/Section_Articles/article_ThreadingGlobalInterpreter.html
这些引号特别引人注意:
Every ten instructions (this default can be changed), the core releases the GIL for the current thread. At that point, the OS chooses a thread from all the threads competing for the lock (possibly choosing the same thread that just released the GIL – you don't have any control over which thread gets chosen); that thread acquires the GIL and then runs for another ten bytecodes.
和
Note carefully that the GIL only restricts pure Python code. Extensions (external Python libraries usually written in C) can be written that release the lock, which then allows the Python interpreter to run separately from the extension until the extension reacquires the lock.
听起来GIL只是为上下文切换提供了更少的可能实例,并使多核/处理器系统对于每个python解释器实例而言都表现为单个核,因此,是的,我们仍然需要使用同步机制。
全局解释器锁定可防止线程同时访问解释器(因此CPython只能使用一个内核)。但是,据我了解,线程仍然被抢先中断和调度,这意味着我们仍然需要对共享数据结构进行锁定,以免线程被彼此的脚趾踩伤。
我一次又一次遇到的答案是,因此,Python中的多线程很少值得开销。我听说过PyProcessing项目的好处,该项目使多个进程像共享线程一样,以多线程的方式"简单"地运行。 。)这使我们可以绕过GIL,因为每个进程都有自己的解释器。
如果我们在线程之间共享状态,则仍然需要锁。 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")之间中断,从而导致更新丢失。结果是共享状态的随机值。
为了使更新一致,运行方法将需要将共享状态锁定在读取-修改-写入部分(循环内部)中,或者需要某种方法来检测共享状态自读取以来何时发生了变化。
添加到讨论中:
因为GIL存在,所以某些操作在Python中是原子的,不需要锁。
http://www.python.org/doc/faq/library/#what-kinds-of-global-value-mutation-are-thread-safe
但是,如其他答案所述,无论何时应用程序逻辑需要锁,我们仍需要使用锁(例如在生产者/消费者问题中)。
威尔·哈里斯(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确实在任何时间都只允许一个线程(时间切片线程),那么应该就不可能存在过时的值,对吗?
这样想:
在单处理器计算机上,多线程是通过挂起一个线程并以足够快的速度启动另一个线程以使其看起来同时运行来发生的。这就像带有GIL的Python:实际上只有一个线程在运行。
问题是线程可以挂在任何地方,例如,如果我要计算b =(a + b)* 3,则可能会产生类似以下的指令:
1 a += b 2 a *= 3 3 b = a
现在,假设该线程正在一个线程中运行,并且该线程在第1行或者第2行之后被挂起,然后另一个线程进入并运行:
b = 5
然后,当另一个线程恢复时,b被旧的计算值覆盖,这可能不是预期的。
因此,我们可以看到,即使它们实际上并没有同时运行,我们仍然需要锁定。