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

时间:2019-05-21 19:54:11

标签: 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;
}

这是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),感觉就像是一个骇人的骇客。我们可以不执行此操作而设置任务运行吗?一定有更好的方法...

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}")

2 个答案:

答案 0 :(得分:2)

我认为您的测试是不言而喻的。 Python中awaitasync的前身是生成器(in 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

另请参阅此帖子:In practice, what are the main uses for the new "yield from" syntax in Python 3.3?


我注意到您的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}")

现在执行时间应该相同。

答案 1 :(得分:0)

因此,通过更多的研究,似乎可以实现,但并不像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}")

与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()

我不太喜欢必须记住添加额外的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}")