Основи asyncio

11 хв. читання

Модуль 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. Там можна знайти багато прикладів використань цієї бібліотеки. Також не слід обходити стороною офіційну документацію.

Додаткові матеріали

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.8K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація