在某些情况下,Python线程可以安全地操作共享状态吗?

时间:2010-04-29 20:06:43

标签: python multithreading gil

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

关于Python中的线程的每篇this文章,我有几个可靠的,可测试的例子,当多个线程访问共享状态时可能会发生陷阱。此页面上提供的示例竞争条件涉及读取和操作存储在字典中的共享变量的线程之间的竞争。我认为这场比赛的情况非常明显,幸运的是显然是可以测试的。

但是,我无法通过列表附加或变量增量等原子操作来唤起竞争条件。这项测试详尽地试图展示这样一个种族:

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直接相关吗?

2 个答案:

答案 0 :(得分:7)

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

其他操作也不一定如此。 Python中的大量操作可能会导致执行任意Python代码,从而导致GIL被释放。例如,i += 1是三个不同的操作,“获取i”,“添加1”和“将其存储在i”。“添加1”将会翻译(在这种情况)进入it.__iadd__(1),可以随心所欲地做任何事情。

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

答案 1 :(得分:1)

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

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