7

我知道 Python 线程一次只能执行一个字节码,那么为什么线程库会提供锁呢?我假设如果一次只执行一个线程,就不会发生竞争条件。

该库提供锁、条件和信号量。这样做的唯一目的是同步执行吗?

更新:

我做了一个小实验:

from threading import Thread
from multiprocessing import Process

num = 0

def f():
    global num
    num += 1

def thread(func):
    # return Process(target=func)
    return Thread(target=func)


if __name__ == '__main__':
    t_list = []
    for i in xrange(1, 100000):
        t = thread(f)
        t.start()
        t_list.append(t)

    for t in t_list:
        t.join()

    print num

基本上我应该启动 100k 线程并递增 1。返回的结果是 99993。

a) 如果有 GIL 同步并避免竞争条件,结果怎么可能不是 99999?b) 甚至可以启动 100k OS 线程吗?

更新2,看到答案后:

如果 GIL 并没有真正提供一种方法来执行像原子递增这样的简单操作,那么将它放在那里的目的是什么?它对解决令人讨厌的并发问题没有帮助,那么为什么要实施它呢?我听说过 C 扩展的用例,有人可以举例说明吗?

4

2 回答 2

11

GIL 同步字节码操作。一次只能执行一个字节码。但是如果你有一个需要多个字节码的操作,你可以在字节码之间切换线程。如果您需要操作是原子的,那么您需要在 GIL 之外进行同步。

例如,递增整数不是单个字节码:

>>> def f():
...   global num
...   num += 1
...
>>> dis.dis(f)
  3           0 LOAD_GLOBAL              0 (num)
              3 LOAD_CONST               1 (1)
              6 INPLACE_ADD
              7 STORE_GLOBAL             0 (num)
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE

这里需要四个字节码来实现num += 1。GIL 不会确保 x 原子地递增。您的实验证明了这个问题:您丢失了更新,因为线程在 LOAD_GLOBAL 和 STORE_GLOBAL 之间切换。

GIL 的目的是确保 Python 对象的引用计数自动递增和递减。它并不是要帮助您使用自己的数据结构。

于 2014-11-11T20:18:37.583 回答
3

Python 的原生线程在字节码级别工作。也就是说,在每个字节码之后(实际上,我相信字节码的数量是可配置的),一个线程可能会将控制权交给另一个线程。

对不是单个字节码的共享资源的任何操作都需要锁定。即使给定的操作在某个版本的 CPython 中是单个字节码,但在每个解释器的每个版本中可能并非如此,所以无论如何你最好使用锁。

实际上,除了在 VM 级别而不是在硬件级别之外,您需要锁定的原因与此相同。

于 2014-11-11T20:13:04.390 回答