另一个问题中的一些讨论鼓励我更好地理解在多线程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()我已经运行了上面的测试,没有失败(100x100k多线程追加)。有人能让它失败吗?有没有另一类对象可以通过原子的、增量的、线程的修改来使其行为不端?
这些隐含的“原子”语义是否适用于Python中的其他操作?这与GIL有直接关系吗?
发布于 2010-04-30 04:17:04
追加到列表是线程安全的,是的。您只能在持有GIL的同时附加到列表中,并且list会注意在append操作期间不释放GIL (毕竟,这是一个相当简单的操作)。不同线程的append操作的顺序当然是可以抓取的,但它们都是严格序列化的操作,因为GIL永远不会在append期间被释放。
其他操作不一定也是如此。Python中的大量操作可能导致执行任意Python代码,进而导致GIL被释放。例如,i += 1是三个不同的操作,"get i“,"add 1 to it”和"store it in i“。"add 1 to it”将翻译(在本例中)为it.__iadd__(1),它可以去做它想做的任何事情。
Python对象本身会保护它们自己的内部状态-- dict不会因为两个不同的线程试图在其中设置项而被破坏。但是,如果dict中的数据被认为是内部一致的,那么dict和GIL都不会采取任何措施来保护这一点,除非(以通常的线程方式)使其不太可能发生,但仍然有可能出现与您想象的不同的结果。
发布于 2010-04-30 04:34:41
在CPython中,线程切换是在执行了sys.getcheckinteval()字节码之后完成的。因此,上下文切换永远不会在单个字节码的执行过程中发生,并且编码为单个字节码的操作本质上是原子的和线程安全的,除非该字节码执行其他Python代码或调用释放GIL的C代码。对内置集合类型(dict、list等)的大多数操作都属于“固有线程安全”类别。
然而,这是一个特定于Python的C实现的实现细节,不应该依赖。其他版本的Python (Jython、IronPython、PyPy等)可能不会以相同的方式运行。也不能保证CPython的未来版本会保留这种行为。
https://stackoverflow.com/questions/2740435
复制相似问题