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

9

asyncio 文档 中提到:

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

能否有人解释一下这句话,或者举个例子说明误用 asyncio 如何导致在线程间共享的对象上进行不同步的写入?我认为 GIL 意味着一次只有一个线程可以运行解释器,因此发生在解释器中的事件(如读写 Python 对象)在线程之间是自动同步的。

上面引用中的第二句话听起来像是一个线索,但我不确定它的意义。

我想,一个线程总是可以通过释放 GIL 并决定写入 Python 对象来造成混乱,但这并不特定于 asyncio,因此我不认为文档在这里所指的就是这个。

这可能是关于 asyncio PEP 保留某些 asyncio 对象不是线程安全的选项,尽管目前在 CPython 中实现恰好是线程安全的吗?


2
一些操作需要多个指令进行同步,其中Python可以由不同的线程解释。GIL从未简单地同步Python程序,与asyncio无关。--它只是确保Python对象在C级别上是线程安全的,而不是在Python级别上。 - Niklas R
1个回答

8

事实上,没错,每个线程就是一个新的解释器线程。

它是由操作系统管理的真正的线程,而不是仅在Python虚拟机内部用于Python代码的内部管理线程。

GIL的作用是防止基于操作系统的线程干扰Python对象。

想象一下,在一个CPU上有一个线程,另一个CPU上有另一个线程。纯并行的线程,用汇编语言编写。两个线程同时尝试更改寄存器值。这种情况绝对不可取。访问同一内存位置的汇编指令最终会混淆要将什么移动到哪里和何时移动。这样操作的结果最终可能很容易导致分段错误。好吧,如果我们用C语言编写,则C语言控制该部分,以使其不会在C代码中发生。GIL在C级别上对Python代码执行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()将阻塞,直到第一个线程完成对其的迭代。这是线程安全的。


GIL并不控制我们在线程中所做的事情,而只是控制线程之间互相影响的行为。这句话简直是珠玉在前。 - xyres
@al.zatv:是的,注册表术语可能会被错误理解。我所指的C是它创建了一个函数堆栈,在函数内部为局部变量名称创建内存占位符,这些名称稍后将被转换为内存地址和其他一些东西,如果您在纯汇编中编程,则必须手动实现。这些都是C编译时转换的内容。不,操作系统在执行字节码之前或期间没有时间分析代码,以确定您的代码是否破坏了在线程或进程之间共享的已分配内存。因此,操作系统什么都不做。 - Dalen
@al.zatv:我所指的是C语言是我们的内存管理器。C没有GIL,也没有在编译时模拟它,因此它确实可以让您把脚放在任何地方。 C stdlib大多数情况下不是线程安全的。但是,操作系统控制的是系统调用。而stdlib中充满了这些调用,例如printf(),scanf(),open(),sleep(),alloc(),free()等等。例如,如果您在线程中使用printf(),则可能会根据操作系统的优先级在屏幕上打印混乱的行,但您不会因为内存重叠而得到混合的字母或两行交织在一起。 - Dalen
@Dalen OpenGL和DirectX有两个部分:用户空间库和驱动程序。它们通过系统调用进行通信(或类似方式)。因此,所有的脏活累活都由驱动程序完成。据我所知,SDL是在OpenGL/DirectX之上的另一层。 - al.zatv
@al.zatv:我说的重点不是这个。顺便说一下,你可以将SDL作为一堆共享库与你的应用程序一起分发,因此,SDL本身的驱动程序不会被安装。当然,这些库,特别是DirectX,使用操作系统的本地图形系统,这些系统运行在内核空间中,但是,这并不是我的重点。请不要太偏离问题的上下文。 - Dalen
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接