【问题标题】:Are there some cases where Python threads can safely manipulate shared state?在某些情况下,Python 线程可以安全地操作共享状态吗?
【发布时间】:2011-02-13 23:43:46
【问题描述】:

另一个问题中的一些讨论鼓励我更好地理解多线程 Python 程序中需要锁定的情况。

根据this 关于 Python 中的线程的文章,我有几个可靠的、可测试的示例说明多个线程访问共享状态时可能发生的陷阱。本页提供的示例竞争条件涉及读取和操作存储在字典中的共享变量的线程之间的竞争。我认为在这里进行比赛的情况非常明显,幸运的是非常可测试。

但是,我无法通过诸如列表追加或变量增量之类的原子操作来引发竞争条件。该测试详尽地试图证明这样的比赛:

from threading import Thread, Lock
import operator

def contains_all_ints(l, n):
    l.sort()
    for i in xrange(0, n):
        if l[i] != i:
            return False
    return True

def test(ntests):
    results = []
    threads = []
    def lockless_append(i):
        results.append(i)
    for i in xrange(0, ntests):
        threads.append(Thread(target=lockless_append, args=(i,)))
        threads[i].start()
    for i in xrange(0, ntests):
        threads[i].join()
    if len(results) != ntests or not contains_all_ints(results, ntests):
        return False
    else:
        return True

for i in range(0,100):
    if test(100000):
        print "OK", i
    else:
        print "appending to a list without locks *is* unsafe"
        exit()

我已经运行了上面的测试,没有失败(100x 100k 多线程追加)。谁能让它失败?是否有另一类对象可以通过原子的、增量的、线程修改来使其行为不端?

这些隐含的“原子”语义是否适用于 Python 中的其他操作?这是否与 GIL 直接相关?

【问题讨论】:

  • 测试不是证明并发应用程序正确性的有效方法。对于任何特定的测试来说,以一种永远不会触发问题的可预测的方式交错执行是非常容易的,但是对代码的最微小的更改(即,将其扩展到现实世界的情况时)可能会立即显示出缺陷。并发软件的正确性应该被证明,而不是测试。

标签: python multithreading gil


【解决方案1】:

附加到列表是线程安全的,是的。您只能在持有 GIL 时追加到列表,并且列表注意不要在 append 操作期间释放 GIL(毕竟这是一个相当简单的操作。)order其中不同线程的追加操作当然是可以争取的,但它们都将是严格的序列化操作,因为在追加期间永远不会释放 GIL。

其他操作不一定如此。 Python 中的许多操作会导致任意 Python 代码被执行,这反过来又会导致 GIL 被释放。例如,i += 1 是三个不同的操作,“获取i”、“向其添加 1”和“将其存储在 i”。“向其添加 1”将(在这种情况下)转换为 @987654325 @,它可以随心所欲地为所欲为。

Python 对象本身会保护自己的内部状态——字典不会被试图在其中设置项目的两个不同线程破坏。但是,如果 dict 中的数据应该是内部一致的,那么 dict 和 GIL 都不会做任何事情来保护它,除非(以通常的线程方式)通过使其不太可能但仍然可能事情结束和你想象的不一样。

【讨论】:

    【解决方案2】:

    在 CPython 中,线程切换是在 sys.getcheckinteval() bycodes 被执行时完成的。因此,在单个字节码的执行过程中永远不会发生上下文切换,并且编码为单个字节码的操作本质上是原子和线程安全的,除非该字节码执行其他 Python 代码或调用释放 GIL 的 C 代码。内置集合类型(dict、list 等)上的大多数操作都属于“固有线程安全”类别。

    但是,这是特定于 Python 的 C 实现的实现细节,不应依赖。其他版本的 Python(Jython、IronPython、PyPy 等)的行为方式可能不同。也不能保证未来版本的 CPython 会保持这种行为。

    【讨论】:

    • 这符合我对正在发生的事情的直觉,但我从没想过要检查其他实现。我只使用 CPython。我确实考虑过,为了使代码能够适应替代实现的未来,不应该依赖这个细节。
    • 字节码的区别大多不是很有趣,因为几乎所有的字节码都有可能执行更多的 Python 代码,而有些则没有可能自己释放 GIL。例如,list.append() 不是一个字节码,而实际的append 工作由CALL_FUNCTION 操作码执行,这极有可能执行更多代码:-)
    猜你喜欢
    • 2015-02-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-02
    • 2020-10-02
    • 2015-12-19
    相关资源
    最近更新 更多