首页 > 解决方案 > 现在开始异步任务,稍后等待

问题描述

C# 程序员试图学习一些 Python。我正在尝试运行 CPU 密集型计算,同时让 IO 绑定异步方法在后台悄悄地消失。在 C# 中,我通常会设置可等待运行,然后启动 CPU 密集型代码,然后等待 IO 任务,然后合并结果。

这是我在 C# 中的做法

static async Task DoStuff() {
    var ioBoundTask = DoIoBoundWorkAsync();
    int cpuBoundResult = DoCpuIntensizeCalc();
    int ioBoundResult = await ioBoundTask.ConfigureAwait(false);

    Console.WriteLine($"The result is {cpuBoundResult + ioBoundResult}");
}

static async Task<int> DoIoBoundWorkAsync() {
    Console.WriteLine("Make API call...");
    await Task.Delay(2500).ConfigureAwait(false); // non-blocking async call
    Console.WriteLine("Data back.");
    return 1;
}

static int DoCpuIntensizeCalc() {
    Console.WriteLine("Do smart calc...");
    Thread.Sleep(2000);  // blocking call. e.g. a spinning loop
    Console.WriteLine("Calc finished.");
    return 2;
}

这是python中的等效代码

import time
import asyncio

async def do_stuff():
    ioBoundTask = do_iobound_work_async()
    cpuBoundResult = do_cpu_intensive_calc()
    ioBoundResult = await ioBoundTask
    print(f"The result is {cpuBoundResult + ioBoundResult}")

async def do_iobound_work_async(): 
    print("Make API call...")
    await asyncio.sleep(2.5)  # non-blocking async call
    print("Data back.")
    return 1

def do_cpu_intensive_calc():
    print("Do smart calc...")
    time.sleep(2)  # blocking call. e.g. a spinning loop
    print("Calc finished.")
    return 2

await do_stuff()

重要的是,请注意 CPU 密集型任务由无法等待的阻塞睡眠表示,IO 绑定任务由可等待的非阻塞睡眠表示。

在 C# 中运行需要 2.5 秒,在 Python 中运行需要 4.5 秒。不同之处在于 C# 立即运行异步方法,而 python 仅在遇到等待时才启动该方法。下面的输出证实了这一点。我怎样才能达到预期的结果。如果可能的话,将不胜感激在 Jupyter Notebook 中工作的代码。

--- C# ---
Make API call...
Do smart calc...
Calc finished.
Data back.
The result is 3
--- Python ---
Do smart calc...
Calc finished.
Make API call...
Data back.
The result is 3

更新 1

受 knh190 的回答启发,我似乎可以使用asyncio.create_task(...). 这达到了预期的结果(2.5 秒):首先,异步代码设置为运行;接下来,阻塞CPU代码同步运行;第三,等待异步代码;最后合并结果。为了让异步调用真正开始运行,我必须输入一个await asyncio.sleep(0),这感觉就像一个可怕的 hack。我们可以在不这样做的情况下设置任务运行吗?一定会有更好的办法...

async def do_stuff():
    task = asyncio.create_task(do_iobound_work_async())
    await asyncio.sleep(0)  #   <~~~~~~~~~ This hacky line sets the task running

    cpuBoundResult = do_cpu_intensive_calc()
    ioBoundResult = await task

    print(f"The result is {cpuBoundResult + ioBoundResult}")

标签: pythonasync-awaitjupyter-notebook

解决方案


我认为您的测试几乎是不言自明的。Python 的前身是生成awaitasync在 Python 2 中)。Python 只创建一个协程,但在您显式调用它之前不会启动它。

因此,如果您想像 C# 一样立即触发协程,则需要将await行向前移动。

async def do_stuff():
    ioBoundTask = do_iobound_work_async() # created a coroutine
    ioBoundResult = await ioBoundTask     # start the coroutine
    cpuBoundResult = do_cpu_intensive_calc()
    print(f"The result is {cpuBoundResult + ioBoundResult}")

这相当于:

def do_stuff():
    # create a generator based coroutine
    # cannot mix syntax of asyncio
    ioBoundTask = do_iobound_work_async()
    ioBoundResult = yield from ioBoundTask
    # whatever

另见这篇文章:在实践中,Python 3.3 中新的“yield from”语法的主要用途是什么?


我注意到您的 C# 和 Python 并不严格等价。Python 中只有 asyncio.Task 是并发的:

async def do_cpu_intensive_calc():
    print("Do smart calc...")
    await asyncio.sleep(2)
    print("Calc finished.")
    return 2

# 2.5s
async def do_stuff():
    task1 = asyncio.create_task(do_iobound_work_async())
    task2 = asyncio.create_task(do_cpu_intensive_calc())

    ioBoundResult = await task1
    cpuBoundResult = await task2
    print(f"The result is {cpuBoundResult + ioBoundResult}")

现在执行时间应该是一样的。


推荐阅读