Я.Студент. Асинхронность и параллелизм

16 Apr 2014

Я.Студент: Асинхронность и параллелизм

Лекция Дмитрия Курилова в ИМКН УрФУ из цикла Я.Студент в УрФУ.

Постепенное усложнение программ привело к одной из наиболее острых проблем современного ПО – блокирующему вводу/выводу. Для её решения требуются подходы, позволяющие программе работать асинхронно и не допускающие простоя процессора. Другой важной проблемой, возникшей из-за отказа от частотного масштабирования и появления многоядерных систем, стала необходимость эффективной утилизации всех имеющихся в компьютере вычислителей. Другими словами, современные программы должны разбивать задачи на подзадачи и вычислять их параллельно.

Все в пару не влезло, поэтому будем рассматривать проблемы асинхронности и только на примере веба.

Неблокирующая обработка 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
  • Каждая модель хороша по своему
  • Надо попробовать всё.