Async: за кулисами абстракций

Almaty Python Meetup #4

Д
Даниал

async/await в Python выглядят простыми — пока всё работает. За этими ключевыми словами скрываются событийный цикл, корутины и неблокирующий ввод-вывод, и понимание того, что происходит «под капотом», помогает писать асинхронный код осознанно, а не по шаблону.

Даниал разбирает асинхронность изнутри — от процессов и потоков до того, как из генераторов вырастает async/await.

В докладе: — процессы, потоки и адресное пространство: чем они отличаются и почему переключение контекста стоит дорого — конкурентность против параллелизма и кооперативная многозадачность — генераторы как основа корутин: send, yield и yield from — модели ввода-вывода: blocking, non-blocking и I/O-мультиплексирование (select, poll, epoll, kqueue) — как из этого складывается событийный цикл и почему Async = non-blocking I/O + мультиплексирование + кооперативная многозадачность — async/await как синтаксический сахар над генераторами

Презентация

Слайд 1: Async 1 / 49
Текст презентации

Слайд 1: Async

Async За кулисами абстракций

Слайд 2: Кто я?

Кто я?

Слайд 3: Предисловие

Предисловие Данный доклад не является референсом к документации asyncio, в докладе будут рассмотрены типичные сценарии использования высокоуровневого API с некоторыми деталями реализации, за более обширными и конкретными юзкейсами стоит обратиться к документации. Цель доклада: дать правильный набор абстракций для вхождения в тему асинхронного программирования и пищу для дальнейших размышлений.

Слайд 4: Общая картина

Общая картина Порядок не совсем верный (или совсем неверный, решайте сами)

Слайд 5: Откуда ноги растут: процессы

Откуда ноги растут: процессы Процесс, если просто–исполняемая программа. У процесса есть свое адресное пространство, регистры (контекст) и состояние. (Виртуальное) адресное пространство процесса–диапазон адресов, которые выделены процессу. Эти адреса лежат в разных секциях/областях памяти, какие-то отвечают за код исполняемой программы, какие-то за глобальные неинициализированные данные и т.д.

Слайд 6: Иллюстрация процесса и его

Иллюстрация процесса и его адресного пространства. Адреса в адресных пространствах нескольких процессов ничего не знают друг о друге. Несколько процессов могут совместно использовать одно общее адресное пространство, такие называются потоками.

Слайд 7: Откуда ноги растут: потоки

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

Слайд 8: Различие между однопоточным процессом (программой) и многопоточным

Различие между однопоточным процессом (программой) и многопоточным

Слайд 9: Какие проблемы решают?

Какие проблемы решают?

Слайд 10: Какие проблемы возникают

Какие проблемы возникают 1. Спавнить процессы–дорого (потоки дешевле, но на N задач это будет накладно) 2. Переключать контекст процессов и потоков–дорого, потому что нам нужно восстановить регистры 3. Планирование процессов и потоков–дорого. 4. Т.к. у потоков и процессов есть адресное пространство, на каждую единицу нужно выделить свой стек, кучу, регистры–дорого. 5. Блокировки и доступ к общей памяти–дорого и нетривиально в комплексных системах.

Слайд 11: Немного про переключение контекста

Немного про переключение контекста

Слайд 12: Конкурентность/параллелизм

Конкурентность/параллелизм Конкурентность связана с управлением несколькими задачами одновременно. Задачи могут начинаться, выполняться и завершаться в перекрывающиеся периоды времени, но не обязательно в одно и то же мгновение. В Python конкурентность обеспечивается при помощи Threading API и корутин. Параллелизм относится к одновременному выполнению нескольких вычислений. Это техника выполнения двух или более задач или вычислений одновременно, используя несколько процессоров или ядер в компьютере для выполнения нескольких операций параллельно. В Python обеспечивается при помощи multiprocessing и threading (с оговорками).

Слайд 13: Конкурентность/параллелизм

Конкурентность/параллелизм ByteByteGo on parallelism vs concurrency

Слайд 14: Виды многозадачности

Виды многозадачности Грубо говоря–задача сама говорит, в какой момент времени она засыпает и передает управление вызывающему коду. Такой подход называется кооперативной многозадачностью. Если задача сама не хочет отдавать управление вызывающему коду или не делает какие-либо системные вызовы, ОС (или другая вызывающая/исполняющая среда) может забрать управление самостоятельно, без участия задачи. (в контексте ОС–при помощи прерывания от таймера).

Слайд 15: Генераторы!!!

Генераторы!!! Генератор–реализация итератора, с методами send, throw и close. Сохраняет состояние и засыпает, когда код доходит до каждого встреченного yield.

Слайд 16: Бесконечная сумма

Бесконечная сумма

Слайд 17: Такие разные модели I/O

Такие разные модели I/O ● Blocking I/O ● Non-blocking I/O ● I/O multiplexing (a.k.a) I/O polling–select, pselect, poll, epoll, kevent, kqueue, IOCP proactor (windows) ● Signal-driven I/O (SIGIO) ● Asynchronous–AIO POSIX interface

Слайд 18: От userspace до kernelspace

От userspace до kernelspace

Слайд 19: От userspace до kernelspace

От userspace до kernelspace

Слайд 20: Файловые дескрипторы и сокеты

Файловые дескрипторы и сокеты

Слайд 21: Blocking I/O

Blocking I/O

Слайд 22: Синхронный блокирующий HTTP-клиент

Синхронный блокирующий HTTP-клиент

Слайд 23: Non-blocking I/O

Non-blocking I/O

Слайд 24: yield sock–”я подожду, пока в сокете появятся данные”

yield sock–”я подожду, пока в сокете появятся данные”

Слайд 25: Регистрация событий и добавление тасков

Регистрация событий и добавление тасков

Слайд 26: I/O multiplexing

I/O multiplexing

Слайд 27: I/O multiplexing

I/O multiplexing

Слайд 28: I/O multiplexing

I/O multiplexing

Слайд 29: Обработка событий и поллинг дескрипторов

Обработка событий и поллинг дескрипторов

Слайд 30: Запуск задач

Запуск задач

Слайд 31: Async = Non-blocking I/O + edge-triggered I/O multiplexing +

Async = Non-blocking I/O + edge-triggered I/O multiplexing + Cooperative multitasking Non-blocking I/O = fcntl/ioctl сисколлы + O_NONBLOCK/O_ASYNC флаги на файловых и/или сокет дескрипторах I/O multiplexing = select/pselect/poll/epoll/kqueue/kevent/etc. Cooperative multitasking = generators (корутины) (в контексте питона)

Слайд 34: Недостатки I/O multipexers

Недостатки I/O multipexers select: ● требуется 2 сисколла ● ограниченное кол-во файловых дескрипторов (1024) (можно поменять в FD_SETSIZE) ● временная сложность – O(n), где n–кол-во файловых дескрипторов ● Ядро каждый бегает по FD’s и регистрирует/перерегестрирует каждый–дорого epoll: ● Тяжело скейлить на треды: нужно использовать EPOLLEXCLUSIVE/EPOLLONESHOT флаги ● Используется 3 сисколла: epoll_wait, epoll_ctl, epoll_create ● Не умеет работать с обычными файлами The Linux Programming Interface book

Слайд 36: send(), или как в корутину передать значение

.send(), или как в корутину передать значение https://www.fluentpython.com/extra/classic-coroutines/

Слайд 37: Суммы, суммы, еще суммы

Суммы, суммы, еще суммы

Слайд 38: tiny tiny event loop

tiny tiny event loop https://github.com/megahomyak/tiny_event_loop/ blob/main/event_loop.py

Слайд 39: Fluent python. Ch. 21–asynchronous programming

Fluent python. Ch. 21–asynchronous programming

Слайд 40: yield from, или как вызывать вложенные корутины

yield from, или как вызывать вложенные корутины

Слайд 41: yield from, или как работает механика await

yield from, или как работает механика await https://www.fluentpython.com/extra/classic-coroutines/

Слайд 42: yield from, или как работает механика await

yield from, или как работает механика await

Слайд 43: async/await–сахар над генераторами

async/await–сахар над генераторами

Слайд 44: async/await–сахар над генераторами

async/await–сахар над генераторами

Слайд 45: async/await–сахар над генераторами

async/await–сахар над генераторами

Слайд 46: async/await–сахар над

async/await–сахар над генераторами

Слайд 47: Так когда же стоит использовать асинк?

Так когда же стоит использовать асинк? ● Для обработки мн-ва конкурентных сетевых соединений (не мн-ва RPS, см. C10K/C10M problem) ● Для межсервисного взаимодействия с непредсказуемым latency ● Для долгоживущих соединений (например, вебсокеты) ● Для медленных сетевых соединений ● Если удобная библиотека реализует асинк протокол

Слайд 48: Когда асинк использовать не нужно?

Когда асинк использовать не нужно? ● При работе с файловым I/O ● При работе с CPU-bound задачами ● При отсутствии экспертизы

Слайд 49: Дальнейшее чтение

Дальнейшее чтение ● io_uring, POSIX AIO API ● greenlets, libev/libevent библиотеки ● colored functions ● C10K/C10M problems ● back pressure a.k.a. flow control problem