Я.Студент: Асинхронность и параллелизм
Лекция Дмитрия Курилова в ИМКН УрФУ из цикла Я.Студент в УрФУ.
Постепенное усложнение программ привело к одной из наиболее острых проблем современного ПО – блокирующему вводу/выводу. Для её решения требуются подходы, позволяющие программе работать асинхронно и не допускающие простоя процессора. Другой важной проблемой, возникшей из-за отказа от частотного масштабирования и появления многоядерных систем, стала необходимость эффективной утилизации всех имеющихся в компьютере вычислителей. Другими словами, современные программы должны разбивать задачи на подзадачи и вычислять их параллельно.
Все в пару не влезло, поэтому будем рассматривать проблемы асинхронности и только на примере веба.
Неблокирующая обработка HTTP запросов
Число пользователей в интернете растет экспоненциально.
Частота перестала расти в 2004 году, с чем прекратилось вертикальное масштабирование.
Пришлось развивать горизонтальное - стало расти число ядер.
На современном i7 в идеальном виде порядок числа запросов - 4000/сек.
Глупо… Плохо… Деньги на ветер.
Надо решать эту проблему.
Для начала ее надо правильно сформулировать.
Проблема блокирующего ввода-вывода (I/O-bound)
Построить работу программы так, чтобы процессор работал как можно больше и не простаивал, когда происходят другие операции.
Надо писать в неблокирующем стиле.
Варианты решения (основные):
- Событийно-ориентированное программирование
- Корутины
- Модель акторов
Событийно-ориентированное программирование
Давайте будем генерировать события и реагировать на них.
Отложенные вызовы
Пример из twisted:
Класс с определенным callback и errback (обработчик вызова и обработчик ошибки).
С этим объектом надо работать явно.
- Используется на низком уровне и клиентской разработке.
- Плохо подходит для сервера
- код становится лапшой маленьких callback’ов
- сложно отлаживать
Корутины
Основная идея в том, что можно в любой момент остановить выполнение программы и отдать процессор кому-то еще. Потом, дождавшись результатов - вернуться и продолжить. Но без callback’ов.
В Python с помощью yield. Можно вставить yield и будет круто. И появилиь методы yield.send() и .throw() позволяющие передать значение внутрь.
def foo():
a = yield "Give me first num"
b = yield "Give me second num"
yield ("Result:", a + b)
>>>a = foo()
>>>a.next()
Give me first num
>>>a.send(1)
"Give me second num"
>>>a.send(2)
("Result:", 3)
Модель акторов
Процессы и сообщения. Хорошо реализовано в Erlang.
На практике никто не отправляет сообщения конкретному процессу и с конкретным сообщением.
Можно создать функцию call(), которая будет отправлять данное сообщение переданному идентификатору процесса и дожидаться результата.
Все в Erlang’е принято оформлять с помощью такого API. (смотри код Эрланга с лекций РОТ)
А вообще, она не только в Эрланге есть, так что не надо огорчаться.
- Позволяет писать код как простой последовательный.
- При этом, он из множества процессов, следовательно, проблемы I/O bound нет.
- Как побочный эффект - все ошибки локализованы. Легко искать проблемы, отлаживать и искать утечки памяти.
Резюме
- Нельзя просто взять и игнорировать I/O bound
- Каждая модель хороша по своему
- Надо попробовать всё.