立即启动异步任务,稍后等待

Big*_* AL 6 python async-await jupyter-notebook

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;
}
Run Code Online (Sandbox Code Playgroud)

这是python中的等效代码

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;
}
Run Code Online (Sandbox Code Playgroud)

Importantly, please note that the CPU intensive task is represented by a blocking sleep that cannot be awaited and the IO bound task is represented by a non-blocking sleep that is awaitable.

This takes 2.5 seconds to run in C# and 4.5 seconds in Python. The difference is that C# runs the asynchronous method straight away whereas python only starts the method when it hits the await. Output below confirms this. How can I achieve the desired result. Code that would work in Jupyter Notebook would be appreciated if at all possible.

--- C# ---
Make API call...
Do smart calc...
Calc finished.
Data back.
The result is 3
Run Code Online (Sandbox Code Playgroud)
--- Python ---
Do smart calc...
Calc finished.
Make API call...
Data back.
The result is 3
Run Code Online (Sandbox Code Playgroud)

Update 1

受knh190答案的启发,似乎我可以使用来获得大部分信息asyncio.create_task(...)。这样可以达到预期的结果(2.5秒):首先,将异步代码设置为运行;接下来,阻塞的CPU代码将同步运行;第三,等待异步代码;最后将结果合并。为了使异步调用真正开始运行,我必须插入一个await asyncio.sleep(0),这简直是骇人听闻的破解。我们可以不执行此操作而设置任务运行吗?肯定有更好的办法...

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()
Run Code Online (Sandbox Code Playgroud)

Big*_* AL 6

因此,通过更多的研究,这似乎是可能的,但不像 C# 那样容易。的代码do_stuff()变为:

async def do_stuff():
    task = asyncio.create_task(do_iobound_work_async())  # add task to event loop
    await asyncio.sleep(0)                               # return control to loop so task can start
    cpuBoundResult = do_cpu_intensive_calc()             # run blocking code synchronously
    ioBoundResult = await task                           # at last, we can await our async code

    print(f"The result is {cpuBoundResult + ioBoundResult}")
Run Code Online (Sandbox Code Playgroud)

与 C# 相比,两个区别是:

  1. asyncio.create_task(...)将任务添加到正在运行的事件循环中所需
  2. await asyncio.sleep(0)暂时将控制权返回给事件循环,以便它可以启动任务。

完整的代码示例现在是:

import time
import asyncio

async def do_stuff():
    task = asyncio.create_task(do_iobound_work_async())  # add task to event loop
    await asyncio.sleep(0)                               # return control to loop so task can start
    cpuBoundResult = do_cpu_intensive_calc()             # run blocking code synchronously
    ioBoundResult = await task                           # at last, we can await our async code

    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. Hence the use of asyncio
    print("Data back.")
    return 1

def do_cpu_intensive_calc():
    print("Do smart calc...")
    time.sleep(2)  # long blocking code that cannot be awaited. e.g. a spinning loop
    print("Calc finished.")
    return 2

await do_stuff()
Run Code Online (Sandbox Code Playgroud)

我不太喜欢必须记住添加额外内容await asyncio.sleep(0)才能开始任务。拥有一个像这样自动启动任务运行的可等待函数可能会更简洁,begin_task(...)以便可以在稍后阶段等待它。例如,如下所示:

async def begin_task(coro):
    """Awaitable function that adds a coroutine to the event loop and sets it running."""
    task = asyncio.create_task(coro)
    await asyncio.sleep(0)
    return task

async def do_stuff():
    io_task = await begin_task(do_iobound_work_async())
    cpuBoundResult = do_cpu_intensive_calc()
    ioBoundResult = await io_task
    print(f"The result is {cpuBoundResult + ioBoundResult}")
Run Code Online (Sandbox Code Playgroud)