Когда использовать, а когда не использовать Python 3.5 `await`?

Я получаю поток использования asyncio в Python 3.5, но я не видел описания того, что я должен быть awaitЯ и вещи не должны быть или где это было бы незначительным. Должен ли я просто использовать мое лучшее суждение с точки зрения "это операция ввода-вывода и, следовательно, должно быть awaitред "?

2 ответа

Решение

По умолчанию весь ваш код является синхронным. Вы можете сделать это асинхронным определением функций с async def и "вызов" этой функции с await, Более правильный вопрос: "Когда мне писать асинхронный код вместо синхронного?". Ответ: "Когда вы можете извлечь из этого пользу". В большинстве случаев, как вы отметили, вы получите выгоду, когда будете работать с операциями ввода / вывода:

# Synchronous way:
download(url1)  # takes 5 sec.
download(url2)  # takes 5 sec.
# Total time: 10 sec.

# Asynchronous way:
await asyncio.gather(
    async_download(url1),  # takes 5 sec. 
    async_download(url2)   # takes 5 sec.
)
# Total time: only 5 sec. (+ little overhead for using asyncio)

Конечно, если вы создали функцию, которая использует асинхронный код, эта функция также должна быть асинхронной (должна быть определена как async def). Но любая асинхронная функция может свободно использовать синхронный код. Нет смысла приводить синхронный код к асинхронному без какой-либо причины:

# extract_links(url) should be async because it uses async func async_download() inside
async def extract_links(url):  

    # async_download() was created async to get benefit of I/O
    html = await async_download(url)  

    # parse() doesn't work with I/O, there's no sense to make it async
    links = parse(html)  

    return links

Одна очень важная вещь заключается в том, что любая длинная синхронная операция (например,> 50 мс, трудно сказать точно) заморозит все ваши асинхронные операции за это время:

async def extract_links(url):
    data = await download(url)
    links = parse(data)
    # if search_in_very_big_file() takes much time to process,
    # all your running async funcs (somewhere else in code) will be frozen
    # you need to avoid this situation
    links_found = search_in_very_big_file(links)

Вы можете избежать этого, вызывая долго выполняющиеся синхронные функции в отдельном процессе (и ожидая результата):

executor = ProcessPoolExecutor(2)

async def extract_links(url):
    data = await download(url)
    links = parse(data)
    # Now your main process can handle another async functions while separate process running    
    links_found = await loop.run_in_executor(executor, search_in_very_big_file, links)

Еще один пример: когда вам нужно использовать requests в асинчо. requests.get это просто синхронная долго работающая функция, которую не следует вызывать внутри асинхронного кода (опять же, чтобы избежать зависания). Но он работает долго из-за ввода-вывода, а не из-за длинных вычислений. В этом случае вы можете использовать ThreadPoolExecutor вместо ProcessPoolExecutor чтобы избежать некоторых многопроцессорных издержек:

executor = ThreadPoolExecutor(2)

async def download(url):
    response = await loop.run_in_executor(executor, requests.get, url)
    return response.text

У вас не так много свободы. Если вам нужно вызвать функцию, вам нужно выяснить, обычная ли это функция или сопрограмма. Вы должны использовать await ключевое слово тогда и только тогда, когда вызываемая вами функция является сопрограммой.

Если async функции участвуют там должен быть "цикл событий", который управляет этими async функции. Строго говоря, это не обязательно, вы можете "вручную" запустить async метод отправки значений, но, вероятно, вы не хотите делать это. Цикл событий отслеживает еще не завершенные сопрограммы и выбирает следующую для продолжения работы. asyncio Модуль обеспечивает реализацию цикла событий, но это не единственно возможная реализация.

Рассмотрим эти две строки кода:

x = get_x()
do_something_else()

а также

x = await aget_x()
do_something_else()

Семантика абсолютно та же: вызов метода, который выдает какое-то значение, когда значение готово, присваивает его переменной x и делать что-то еще. В обоих случаях do_something_else Функция будет вызываться только после завершения предыдущей строки кода. Это даже не означает, что до, после или во время выполнения асинхронного aget_x Метод управления будет передан в цикл событий.

Тем не менее есть некоторые различия:

  • второй фрагмент может появиться только внутри другого async функция
  • aget_x функция не обычная, а сопрограмма (которая либо объявляется с async ключевое слово или оформлен как сопрограмма)
  • aget_x способен "связываться" с циклом событий: то есть приносить ему несколько объектов. Цикл обработки событий должен иметь возможность интерпретировать эти объекты как запросы для выполнения некоторых операций (например, для отправки сетевого запроса и ожидания ответа, или просто приостановить эту сопрограмму для n секунд). Обычный get_x Функция не может связаться с циклом событий.
Другие вопросы по тегам