Запустите асинхронную задачу сейчас, ждите позже - PullRequest
6 голосов
/ 21 мая 2019

C # программист пытается выучить немного Python.Я пытаюсь запустить интенсивный процесс вычисления, позволяя асинхронному методу, связанному с вводом-выводом, тихонько перенести фоновый режим.В C # я обычно устанавливаю ожидаемый ход, затем запускаю код, интенсивно использующий процессор, затем жду задачу ввода-вывода и затем объединяю результаты.

Вот как я это сделаю в 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()

Важно отметить, что задача с интенсивным использованием ЦП представлена ​​блокирующим сном, которого нельзя ожидать, а задача, связанная с вводом-выводом, представлена ​​неблокирующейожидаемый сон.

Это займет 2,5 секунды для запуска в C # и 4,5 секунды в Python.Разница в том, что 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 с): во-первых, асинхронный код запускается;затем блокирующий код ЦП выполняется синхронно;в-третьих, ожидается асинхронный код;наконец результаты объединены.Чтобы асинхронный вызов действительно начал работать, мне нужно было ввести 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 ]

2 голосов
/ 22 мая 2019

Я думаю, что ваш тест говорит сам за себя. Предшественником для await и async в Python был генератор ( в 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

Также см. Этот пост: На практике, каковы основные причины использования нового синтаксиса "yield from" в Python 3.3?


Я заметил, что ваш C # и Python не являются строго эквивалентными. Одновременно только asyncio.Task в Python:

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

Теперь время выполнения должно быть таким же.

0 голосов
/ 25 мая 2019

Так что, проведя немного больше исследований, кажется, что это возможно, но не так просто, как в 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}")
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...