【问题标题】:如何同步运行 Python 协程,直到第一次等待,就像在 C# 中一样?
【发布时间】:2022-01-23 15:57:06
【问题描述】:

我正在使用一些 asyncio Python 代码,但遇到了协程排序问题。我对 C# 有一定的经验,所以我会用它作为例子。

在 C# 中我可以编写:

using System;
using System.Threading.Tasks;

public class Program
{
    async static Task<int> foo(int n, int delay)
    {
        Console.WriteLine("Foo start {0}",n);
        await Task.Delay(delay);
        Console.WriteLine("Foo end {0}",n);
        return 0;
    }

    public static void Main()
    {
        Task<int> t1 = foo(1,300);
        Task<int> t2 = foo(2,200);
        
        Task.WaitAll(new []{t2,t1});// Reverse on purpose
    }
}

结果如下,注意协程的开始匹配调用顺序,不是等待顺序。这是由于协程函数如何划分到生成器中而得到保证的。 IE。它们总是同步运行,直到第一个 await

Foo start 1
Foo start 2
Foo end 2
Foo end 1

但是当我在 Python 中做同样的事情时,我会得到不同的顺序。

import asyncio
async def foo(n,delay):
    print(f"Foo start {n}")
    await asyncio.sleep(delay/1000)
    print(f"Foo end {n}")
    return 0


async def main():
    t1 = foo(1,300)
    t2 = foo(2,200)
    
    await asyncio.gather(t2,t1) # Reverse on purpose
    
asyncio.run(main())
Foo start 2
Foo start 1
Foo end 2
Foo end 1

我也知道这是为什么 - 因为 foo(1,300) 返回一个生成器,它在协程等待/调度之前不会启动。

我的问题是如何在 Python 中实现类似 C# 的行为,这意味着协程在调用它们时开始而不会中断,直到第一个 await

我试过了

t1 = asyncio.create_task(foo(1,300))
t2 = asyncio.create_task(foo(2,200))

哪个解决了问题,但调度顺序真的有保证吗?

【问题讨论】:

    标签: python c# python-asyncio


    【解决方案1】:

    你试过这个吗:

    t1 = asyncio.create_task(foo(1,300))
    await asyncio.sleep(0.0)
    t2 = asyncio.create_task(foo(2,200))
    await asyncio.sleep(0.0)
    

    这应该总是有效的,因为它会在每个任务创建后产生事件循环。在创建下一个任务之前,每个任务都应该开始并运行到第一个等待。我说“应该”是因为我自己实际上并没有在实践中这样做过。但我强烈希望它适用于所有情况。

    如果你有很多任务,你可以用这样的小函数清理代码:

    async def start_sequential(*coros):
        for c in coros:
            asyncio.create_task(c)
            await asyncio.sleep(0.0)
    
    c1 = foo(1, 300)
    c2 = foo(2, 200)
    await start_sequential(c1, c2)
    

    【讨论】:

      【解决方案2】:

      asyncio.create_task(),顾名思义,创建一个task.Task 实例。该任务是在task.Task 的构造函数中安排的

      self._loop.call_soon(self.__step, context=self._context)
      

      来自BaseEventLoop.call_soon的文档字符串:

      安排尽快调用回调。 这作为 FIFO 队列运行:回调在 他们注册的顺序。每个回调将是 只调用一次。

      只要您使用不覆盖此行为的事件循环,我会说您可以按照您的意图使用create_task。据我所知,asyncio 中的所有事件循环都没有。

      【讨论】:

      • 感谢您的回答。好的,但取决于 BaseEventLoop 的实现对我来说并不理想。但除非有人提出更好的建议,否则我会接受。
      • 我不确定依赖这种行为的风险有多大,但如果您需要独立于事件循环实现来控制执行顺序,我只会将同步原语视为一种真正的替代方案。由于事件循环是在任何情况下调度执行的...这可能会有点混乱,但肯定会表明意图。
      • @Quimby 另一种选择是简单地反转传递给gather()的协程。与WaitAll() 不同,gather() 有两个目的:将协程提交到事件循环(在 C# 中通过调用 async 函数自动发生),并等待它们全部完成。提交协程时,gather() 将遵循您提供协程的顺序。
      • @user4815162342 谢谢,但我不能依赖gather的命令,真正的代码更复杂,实际上很快就没有gather。我想要通过调用给出的顺序,而不是某个循环调度。
      • @Quimby 明白了。 Asyncio 将始终尊重调用给出的顺序。一个更大的问题是,为什么你严重依赖任务开始执行的顺序——虽然本身没有错,但对我来说这将是一种“设计气味”。启动单独的任务在概念上使操作并行化。如果你需要一些串联的东西,也许你应该使用与任务不同的抽象来编码它们(例如队列)。
      猜你喜欢
      • 2019-09-29
      • 2021-10-10
      • 2017-12-30
      • 2011-03-24
      • 2020-05-13
      • 2016-03-18
      • 1970-01-01
      • 1970-01-01
      • 2011-06-17
      相关资源
      最近更新 更多