考虑到GIL,asyncio如何不是线程安全的?

时间:2017-01-04 08:42:51

标签: python python-multithreading python-asyncio cpython thread-synchronization

asyncio docs读取:

  

大多数asyncio对象都不是线程安全的。只有在访问事件循环之外的对象时才应该担心。

有人可以解释一下这个问题,还是举例说明误用asyncio会导致对线程间共享对象的不同步写入?我认为GIL意味着一次只有一个线程可以运行解释器,所以在解释器中发生的事件,比如读取和编写Python对象,在线程之间进行了简单的同步。

上面引用的第二句听起来像是一条线索,但我不知道该怎么做。

我猜一个线程总是会通过释放GIL并决定写入Python对象来造成破坏,但这并不是特定于asyncio所以我不认为这是文档在这里所指的内容。

这可能是asyncio PEP保留某些asyncio对象的选项不是线程安全的问题,即使CPhthon中的实现恰好是线程安全的吗?

1 个答案:

答案 0 :(得分:3)

实际上,不,每个线程都是解释器的新线程。

这是一个由OS管理的真实线程,而不是Python虚拟机中Python代码的内部管理线程。

需要GIL来防止非常基于操作系统的线程搞乱Python对象。

想象一个CPU上的一个线程和另一个上的另一个线程。纯并行线程,用汇编语言编写。两者同时试图更改注册表值。根本不是理想的情况。访问相同内存位置的汇编指令最终会争先恐后地移动到何处和何时移动。最终,这种动作的结果很容易导致分段错误。好吧,如果我们用C语言编写,C控制那个部分,这样就不会在C代码中发生。 GIL对C级别的Python代码也是如此。因此,实现Python对象的代码在更改它们时不会失去其原子性。想象一个线程将一个值插入到另一个线程中刚刚向下移动的列表中,因为该另一个线程从中移除了一些元素。如果没有GIL,这将会崩溃。

GIL对线程内代码的原子性没有任何作用。它仅适用于内部存储器管理。

即使您拥有像deque()这样的线程安全对象,如果您一次在其上执行多个操作,而没有额外的锁定,您可以从插入其间的另一个线程获得结果。哎呀,问题发生了!

假设一个线程从堆栈中获取一个对象,检查它的某些内容,如果条件是正确的则将其删除。

stack = [2,3,4,5,6,7,8]
def thread1 ():
    while 1:
        v = stack[0]
        sleep(0.001)
        if v%2==0: del stack[0]
        sleep(0.001)

当然,这是愚蠢的,应该使用stack.pop(0)来避免这种情况。但这只是一个例子。

让另一个线程每隔0.002秒添加到堆栈中:

def thread2 ():
    while 1:
        stack.insert(0, stack[-1]+1)
        sleep(0.002)

现在,如果你这样做:

thread(thread2,())
sleep(1)
thread(thread1,())

虽然不太可能,但是会有一个时刻,thread2()尝试在thread1()的检索和删除之间准确地堆叠新项目。因此,thread1()将删除新添加的项而不是正在检查的项。结果不符合我们的意愿。因此,GIL不会控制我们在线程中正在做什么,只是线程在更基本的意义上对彼此做了什么。

想象一下,你为一些活动买了一张买票的服务器。两个用户连接并尝试同时购买相同的票证。如果你不小心,用户可能会坐在另一个上面。

线程安全对象是执行操作的对象,它不允许在第一个操作完成之前执行其他操作。

例如,如果你在一个线程中迭代deque(),并且在其中间另一个线程尝试追加某些东西,append()将阻塞,直到第一个线程完成迭代。这是线程安全的。