为什么 python asyncio loop.call_soon 会覆盖数据?

Why does python asyncio loop.call_soon overwrite data?

我在我们的代码中创建了一个难以追踪的错误,但不明白它为什么会出现。多次推送同一个异步函数以很快调用时会出现问题。同步函数不会发生这种情况。

这是问题的 运行 示例:

import asyncio
import sys

class TestObj(object):

    def __init__(self):

        self.test_data = {'a': 1, 'b': 2, 'c': 3}
        self.loop = asyncio.get_event_loop()
        self.loop.call_later(1, lambda: asyncio.ensure_future(self.calling_func()))
        self.loop.call_later(2, self.calling_func_sync)
        self.loop.call_later(4, sys.exit)
        self.loop.run_forever()

    async def do_something(self, k, v):
        print("Values", k, v)

    async def calling_func(self):
        for k, v in self.test_data.items():
            print("Sending", k, v)
            self.loop.call_soon(lambda: asyncio.ensure_future(self.do_something(k, v)))

    def do_something_sync(self, k, v):
        print("Values_sync", k, v)

    def calling_func_sync(self):
        for k, v in self.test_data.items():
            print("Sending_sync", k, v)
            self.loop.call_soon(self.do_something_sync, k, v)


if __name__ == "__main__":
    a = TestObj()

输出为:

Sending a 1
Sending b 2
Sending c 3
Values c 3
Values c 3
Values c 3
Sending_sync a 1
Sending_sync b 2
Sending_sync c 3
Values_sync a 1
Values_sync b 2
Values_sync c 3

为什么会这样,为什么?只有异步函数被踩踏。我原以为每次调用 call_soon 都会将一个新指针推入堆栈,但似乎有一个指向 self.do_something 的指针被覆盖了。

这与异步代码无关,但与您在循环中创建的 lambda 无关。当您编写 lambda: asyncio.ensure_future(self.do_something(k, v)) 时,您正在创建一个闭包来访问封闭名称空间中的变量 kv(还有 self,但这不是问题)。当调用 lambda 函数时,它将使用调用时外部作用域中那些名称绑定的值, 而不是 它们在定义 lambda 时具有的值。由于 kv 在循环的每次迭代中更改值,这导致所有 lambda 函数看到相同的值(最后一个)。

避免此问题的一种常见方法是将变量的当前值设置为 lambda 函数参数的默认值:

self.loop.call_soon(lambda k=k, v=v: asyncio.ensure_future(self.do_something(k, v)))

您的问题其实与asyncio无关。 lambda: asyncio.ensure_future(self.do_something(k, v)) 中的 kv 仍然引用外部作用域中的变量。它们的值在您调用函数时发生变化:

i = 1
f = lambda: print(i)

f()  # 1
i = 2
f()  # 2

一个常见的解决方案是定义您的函数并(ab)使用默认参数创建一个函数本地变量,该变量在函数创建时保存 i 的值,而不是调用:

i = 1
f = lambda i=i: print(i)

f()  # 1
i = 2
f()  # 1

如果命名让您感到困惑,您可以使用 f = lambda x=i: print(x)

除了纠正其他人关于 lambda 中的错误的解释外,还要注意你甚至不需要 lambda。由于 do_something 是协程,仅调用它不会执行任何代码,直到事件循环的下一次迭代,因此您自动具有 call_soon 的效果。 (这类似于调用生成器函数直到您开始用尽返回的迭代器才开始执行它。)

换句话说,你可以替换

self.loop.call_soon(lambda: asyncio.ensure_future(self.do_something(k, v)))

更简单

self.loop.create_task(self.do_something(k, v))

create_taskpreferable to ensure_future 当你处理协程时。