Модуль asyncio
додали в версії Python 3.4 як тимчасовий пакет. Це означає, що asyncio може отримати зворотно-несумісні зміни або й бути видаленим. Згідно з документацією, asyncio "забезпечує інфраструктуру для написання конкурентних однопотокових додатків з використанням співпрограм, мультиплексування доступу до I/O через сокети та інші ресурси, запуск мережевих клієнтів та серверів і інших схожих задач". В цій статті ви не знайдете всього, що можна робити з допомогою asyncio, натомість ви дізнаєтесь як ним користуватися і чому це буде корисно.
Якщо вам потрібне щось схоже на asyncio, але для старіших версій — погляньте в сторону Twisted чи gevent.
Визначення
Asyncio представляє фреймворк, що працює навколо "циклу подій" (event loop
). Цей цикл зазвичай чекає поки щось відбудеться і потім реагує на подію. Такими подіями можуть бути ввід, вивід (I/O) чи системні події. Також для asyncio існує декілька реалізацій циклу подій. Стандартний в більшості випадків і є найбільш ефективним для даної ОС. Також ви завжди можете явно вказати яку реалізацію використовувати. Якщо коротко, то цикл подій працює так: "коли відбудеться подія А, відреагувати функцією В".
Наприклад, сервер: він чекає поки хтось не попросить ресурс (такий як веб-сторінка) і потім його віддає. Якщо сайт не дуже популярний, то сервер більшість часу буде перебувати в стані спокою, але коли він отримає запит, то повинен відреагувати. Така реакція називається перехопленням подій (event handling
). Коли користувач запитує веб-сторінку, сервер шукає обробника чи декілька обробників цієї події і викликає їх. Коли перехоплювачі закінчать свою роботу, вони повинні повернути управління циклу подій. Щоб реалізувати це в Python asyncio використовує співпрограми (coroutines
).
Співпрограма — це функція, що може передати управління до тієї функції, що її викликала, без втрати стану. Співпрограми є розширенням генераторів. Однією з їх найбільших переваг перед потоками це менше споживання пам'яті. Зауважте, що при виклику функції-співпрограми вона не буде виконана, натомість буде повернуто спеціальний об'єкт, який ви можете передати в цикл подій щоб виконати її зараз чи через деякий час.
Ще одним терміном, що використовується в asyncio є майбутнє (future
). Future
це об'єкт, що відповідає результату роботи, що ще не виконана. Ваш цикл подій може спостерігати за ним і чекати поки робота не буде завершена. Також asyncio підтримує блокування та семафори (locks and semaphores).
Останнім терміном, з яким я хочу вас познайомити буде завдання (Task). Завдання - обгортка для співпрограми і субклас майбутнього. Ви навіть можете запланувати розклад завдань використовуючи цикл подій.
async та await
async
та await
- ключові слова, що були додані в Python 3.5 для реалізації нативних співпрограм і реалізації їх як окремого типу (на відміну від співпрограм, що працюють понад генераторами). Якщо хочете дізнатися більше деталей, зверніться до PEP 492.
В Python 3.4 співпрограми оголошувались так:
import asyncio
@asyncio.coroutine
def my_coro():
yield from func()
Цей декоратор також працює і в Python 3.5, але в модуль types
доданий новий тип, що дозволяє розрізняти нативні співпрограми та генератори. Починаючи з Python 3.5 ви можете оголосити співпрограму за допомогою async def
. Тобто еквівалент співпрограми вище, але з використанням нового синтаксису буде виглядати так:
import asyncio
async def my_coro():
await func()
В середині такої співпрограми ви не можете використовувати yield
, натомість вона повинна містити return
чи await
, що поверне значення з співпрограми. Зауважте, що await
може бути використане лише в співпрограмі.
async/await
можна вважати API для асинхронного програмування. Модуль asyncio
це лише фреймворк. Також є проект curio, що реалізує власний цикл подій на async/await
, не використовуючи asyncio.
Приклад співпрограми
Гарне знання теорії, звісно, важливе, але іноді хочеться побачити все на прикладі. Тож давайте напишемо невеличкий скрипт.
Досить гарним прикладом буде завантаження файлів з інтернету. Зазвичай справа не обмежується одним файлом, тому давайте напишемо співпрограми, що будуть виконувати цю роботу.
import asyncio
import os
import urllib.request
async def download_coroutine(url):
"""
Завантаження файлу по URL
"""
request = urllib.request.urlopen(url)
filename = os.path.basename(url)
with open(filename, 'wb') as file_handle:
while True:
chunk = request.read(1024)
if not chunk:
break
file_handle.write(chunk)
msg = 'Finished downloading {filename}'.format(filename=filename)
return msg
async def main(urls):
"""
Створює групу співпрограм і чекає поки вони фінішують
"""
coroutines = [download_coroutine(url) for url in urls]
completed, pending = await asyncio.wait(coroutines)
for item in completed:
print(item.result())
if __name__ == '__main__':
urls = ["http://www.irs.gov/pub/irs-pdf/f1040.pdf",
"http://www.irs.gov/pub/irs-pdf/f1040a.pdf",
"http://www.irs.gov/pub/irs-pdf/f1040ez.pdf",
"http://www.irs.gov/pub/irs-pdf/f1040es.pdf",
"http://www.irs.gov/pub/irs-pdf/f1040sb.pdf"]
event_loop = asyncio.get_event_loop()
try:
event_loop.run_until_complete(main(urls))
finally:
event_loop.close()
В цьому коді ми імпортуємо потрібні модулі і створюємо нашу першу співпрограму, використовуючи синтаксис async
. Вона називається download_coroutine
і використовує стандартну urllib
щоб завантажити файл, URL якого їй передано. При закінченні буде виведене відповідне повідомлення.
Є ще одна співпрограма — головна. Вона приймає список URL і створює відповідні співпрограми для їх завантаження. Ми використовуємо asyncio.wait
щоб чекати поки співпрограми виконуються. Звісно, щоб вони дійсно виконалися, їх потрібно додати до циклу подій, що ми й робимо за допомогою методу run_until_complete
. Тобто цикл подій запускає головну співпрограму, а вона запускає співпрограми для завантаження.
Планування викликів
Також ви можете створювати цілі черги викликів звичайних функцій в циклі подій. Першим методом, з яким ми познайомимось буде call_soon
. Цей метод викличе ваш калбек відразу як це стане можливим. Він працює як звичайна черга (FIFO, перший прийшов — перший вийшов), тобто якщо якась функція виконується досить довго — інші не почнуть виконуватися поки вона не закінчить. Давайте розглянемо приклад:
import asyncio
import functools
def event_handler(loop, stop=False):
print('Event handler called')
if stop:
print('stopping the loop')
loop.stop()
if __name__ == '__main__':
loop = asyncio.get_event_loop()
try:
loop.call_soon(functools.partial(event_handler, loop))
print('starting event loop')
loop.call_soon(functools.partial(event_handler, loop, stop=True))
loop.run_forever()
finally:
print('closing event loop')
loop.close()
В більшості випадків asyncio не підтримує передачу аргументів до обробника, тому ми будемо використовувати для цього functools.partial
, що бере на вхід функцію та аргументи і повертає таку саму функцію, але вже з "вшитими" аргументами. Наша функція друкує деякий текст до stdout кожен раз, як вона буде викликана. А якщо передати аргумент stop
рівним True
, то вона також зупинить цикл подій.
Перший раз коли ми її викликаємо — ми не зупиняємо цикл. А вже в другому виклику зупинили його. Ми зробили так, бо перед цим сказали йому виконуватися вічно (run_forever
). Коли цикл зупинений — ми можемо його закрити. Якщо ви запустите цей код, то побачите:
starting event loop
Event handler called
Event handler called
stopping the loop
closing event loop
Є ще схода функція call_soon_threadsafe
. Як можна здогадатися, вона працює так само як і call_soon
, але потокобезпечна.
Якщо ви хочете виконати функцію з затримкою — ваш вибір метод call_later
.
loop.call_later(1, event_handler, loop)
Цей метод викличе функцію з затримкою в одну секунду та передать аргументом наш цикл подій.
Якщо ви хочете запустити функцію в конкретний час в майбутньому, ви повинні спершу отримати час циклу, а не комп'ютеру:
current_time = loop.time()
А потім викликати метод call_at
і передати час, в котрий слід виконати функцію, саму функцію та аргументи до неї. Давайте, наприклад, виконаємо функцію через 5 хвилин.
loop.call_at(current_time + 300, event_handler, loop)
Завдання
Завдання (Tasks
) є підкласом Future
і обгорткою над співпрограмами, що дозволяє нам відстежувати їх виконання. Через те, що вони субклас Future
, інші співпрограми також можуть чекати на виконання завдання і ви можете отримати результат після виконання. Давайте розглянемо це на прикладі:
import asyncio
import time
async def my_task(seconds):
"""
Завдання, що займає заданий час
"""
print('This task is taking {} seconds to complete'.format(
seconds))
time.sleep(seconds)
return 'task finished'
if __name__ == '__main__':
my_event_loop = asyncio.get_event_loop()
try:
print('task creation started')
task_obj = my_event_loop.create_task(my_task(seconds=2))
my_event_loop.run_until_complete(task_obj)
finally:
my_event_loop.close()
print("The task's result was: {}".format(task_obj.result()))
Тут ми створили асинхронну функцію, що приймає кількість секунд, які вона повинна виконуватися, це примітивна імітація повільних та великих завдань. Потім ми створюємо наш цикл подій і створюємо об'єкт Task, викликавши метод циклу create_task
і в кінці вказуємо, що цикл повинен виповнюватися до закінчення завдання. В кінці ми отримаємо результат виконання.
Також завдання можна відмінити викликавши його метод cancel
. Якщо ви зупините завдання коли воно чекає на виконання якоїсь операції — буде викликане виключення CancelledError
.
Висновок
Тепер ви повинні знати досить, щоб почати використовувати asyncio. Ця бібліотека дуже потужна і дозволяє робити різні круті речі :). Також загляньте на asyncio.org, де зібрані різні проекти, що використовують asyncio. Там можна знайти багато прикладів використань цієї бібліотеки. Також не слід обходити стороною офіційну документацію.
Додаткові матеріали
- Офіційна документація
- Python Module of the Week: asyncio
- Brett Cannon – Як працює async/await
- StackAbuse – Python async await туторіал
- Medium — Slack-bot на пітоні та asyncio
- Math U Code – Розбираємось в асинхронному I/O з Python 3.4 та Node.js
- Dr Dobbs — Asyncio в Python 3.4: цикли подій
- Effective Python Item 40: Співпрограми для запуску декількох функцій одночасно
- PEP 492 — async/await
Ще немає коментарів