вторник, 16 июля 2013 г.

Жарим deferred'ы.

Основная цель написания сего поста: разобраться в том, как работает декоратор @inlineСallbacks в Twisted и понять, зачем нужны сквозные функции. До этого дело пока не дошло, но разберем подготовительную часть, а именно: зачем нужны Deferred, почему без них будет хуже?

В "поджарке" также будут участвовать:
1. Разница между обычными Deffered'ами и inlineCallbacks. Пример с inlineСallback и без них.
2. Операция cancel. Как она работает и зачем нужна?
3. Отмена Deferred'а по таймауту. Зачем  это нужно и где может использоваться. Пример.




1) Начало.
Рассмотрим разницу между синхронной и асинхронной моделью.
1. Синхронная модель.
Делится на однопоточную и многопоточную.
В однопоточной модели каждая задача выполняется последовательно. Поток выполняет следующую задачу только тогда, когда предыдущая точно завершилась.
В многопоточной модели можно достичь некоторой "одновременной работы" задач, но на каждую задачу выделяется один поток. Деталями управления в многопоточной синхронной модели управляет ОС.
2. Асинхронная модель.
Асинхронная модель всегда будет чередовать выполнение задач. И не важно однопроцессорная у нас система или многопроцессорная, задачи в асинхронной модели чередуются. Взаимодействие задач (их синхронизация) лежит на ответственности программиста.
Подробнее тут.

2) Про reactor.
Для ожидания задач и управления ими, используется внешний цикл, который постоянно ждет каких-либо действий. При возникновении задачи, он немедленно реагирует. Этот цикл лежит в основе шаблона проектирования reactor. Важно заметить, что каждая задача должна быть в неблокирующем режиме.

3) Про callbacks.
Плавно перейдем к callback'ам.
Для написания асинхронного кода, необходимо как-то говорить внешнему циклу о том, какие задачи мы хотим выполнить. Внешний цикл не должен прерываться (т.е. желательно, чтобы наше приложение работало в демонизирующем режиме). А наши задачи не должны быть блокирующими (какой смысл использовать реактор, если мы будем блокировать наш внешний цикл, который как раз и следит за приходом новых задач или выполнением следующих).
Итог:
1. Шаблон реактор - однопоточный.
2. Twisted сам реализует цикл реактора, мы сами не должны его реализовывать.
3. При написании кода, нужно продумывать основную логику задачи.
4. Цикл реактора вызывает наш код в тот момент, когда мы об этом сами скажем. Реактор наперед не может знать, какую часть кода нужно вызвать.
5. Когда наши колбеки выполняются, цикл реактора не выполняется, и наоборот.
6. После возврата из callback'а, цикл реактора возобновляется.
Подробнее тут и тут

Все казалось бы простым, но что делать с исключениями?

4) Про errbacks.
Выполняем мы несколько задач, 1,2,3,4.. и тут Exception. Что делать внешнему циклу? Все приостанавливать или вываливаться с ошибкой? А как же другие задачи? 1 и 2я задачи могут быть никак не связаны между собой. Внешний цикл просто не увидит и не поймет, что произошла какая-то ошибка. И это -- не хорошо.
Выход из этой ситуации: корректно обрабатывать каждое возможное исключение. Т.е. помимо callback'а, который нужно будет выполнить, нужен еще callback, который среагирует на неожиданное исключение. И это решит нашу задачу. В итоге, для каждой задачи нужно писать 2 callback'a.
Это может запутать, если у нас множество задач, которые могут выполняться в одном потоке нашего внешнего цикла. Нужно иметь соответсвие между callback'ом с обычными действиями и callback'ом, который реагирует на ошибку. Этот 2й вид коллбеков назван errback'ом.

5) Примитивный пример:

1. Синхронный код:
try:
text = run_print()
except Exception, err:
    print err
    sys.exit()
else:
    print text
    sys.exit()


2. Асинхронный код:
def print_ok(text):
    print text
    reactor.stop()

def print_fail(err):
    print >>sys.stderr, err
    reactor.stop()

run_print(print_ok, print_err)

reactor.run()

Подробнее тут

6) Deferred
Для того, чтобы не запутаться в сложных ситуациях, в твистед думали думали и придумали абстракцию Deferred.
Deferred содержит внутри цепь из пары callback'ов: первый callback - для правильных результатов, а второй - для ошибочных.

Пример:
from twisted.internet.defer import Deferred

def print_ok(text):
    print text

def print_fail(err):
    print >>sys.stderr, err

d = Deferred()
d.addCallbacks(print_ok, print_fail)
d.callback("Text is correct") # или d.errback(Exception('I have failed.'))
print "End."

reactor.run()

Еще немного о deferred'ах:
1. Один Deferred может быть активным только один раз, затем его нужно удалять.
2. У Deferred'а может быть вызван либо callback, либо errback, но не оба одновременно.
3. Теперь вместо обычного возврата результата, как в синхронном коде, мы должны использовать "асинхронный результат". Deferred - это обещание, которое мы обязаны будем выполнить, как только появится результат.

Теперь немного подробнее об исключениях в асинхронном коде. Подробнее тут.
Но, если коротко и быстро, то попытаемся ответить на следующие вопросы:
1. Один Deferred может содержать в себе цепь из пары callback'ов и errback'ов. Почему? Зачем нам это?
2. Почему один Deferred должен быть активизирован только один раз? Почему только один?
3. Почему внешний цикл не может сам обработать пришедшее к нему исключение?

Ответы на эти вопросы кроются в логике вызовов при возбуждении исключения.
В обычном синхронном коде, когда возникает исключение, мы идем вверх по стеку и сообщаем более высокоуровневому коду о возникшем исключении.
В асинхронном коде вся работа начинается с вызова низкоуровневого реактора (внешего цикла), который может только ждать действий и реагировать на них. Больше ничего не может. Наши callback'и в асинхронном коде -- более высокоуровневые, чем реактор. В итоге, если исключение возникает в нашем callback'е, то исключение начинает передаваться также вверх по стек, но вверху у нас низкоуровневый реактор, который понятия не имеет, что делать с этим исключением. Именно поэтому:
1. Если у нас логика предполагает обработку сразу нескольких исключений, то мы можем для каждого исключения написать свой callback и errback. Получится целая цепь, которая выполняется каждая в своем стек-фрейме.
2. В цепочке из callback'ов и errback'ов в каждый момент времени мы можем идти только по одному пути: либо по callback'у, либо по errback'у. А исключения в errback'ах не генерируются, а просто обрабатываются и логгируются.
3. Реактор не обрабатывает исключения, потому что ничего о них не знает. Реактор -- низкоуровневый метод, а исключения генерируются из более высокоуровневого кода.

*здесь должны быть некоторые доказательства моей писанины*

В каких моментах callback'и в deferred'ах должны быть асинхронными и возвращать свои deferred'ы?

Об этом вы, возможно, узнаете в следующей серии статей про Twisted.



6 комментариев:

  1. Мне непонятно, как из описанных в начале ингридиентов появился шашлык из свинины?

    ОтветитьУдалить
    Ответы
    1. Пожарили деферды -- получили шашлык из свинины. Ты недостаточно абстрагировалась!

      Удалить
  2. >> Внешний цикл не должен прерываться ... А наши задачи не должны быть блокирующими

    >> 5. Когда наши колбеки выполняются, цикл реактора не выполняется, и наоборот.

    я ничего не понял х)

    ОтветитьУдалить
    Ответы
    1. Реактор работает в одном потоке. Он ждет событие. К реактору в гости стучится некоторое событие. Реактор открывает дверь и одновременно, никакого другого действия не может выполнять. Тут наше событие прям сразу засыпает при реакторе. Реактор не понимает, что ему делать с таким событием и тоже засыпает. "Спячка" в данном случае -- блокирующее действие (наподобие sleep()).
      Цикл реактора выполняется -- это означает, что реактор готов принимать некоторые события.

      Удалить
  3. Ответы
    1. run_print() -- это неведомая функция, которая будет просто что-то печатать. В синхронном коде она не принимает ничего, в асинхронном -- внутрь нее должен передаваться коллбек и еррбек. Ну это некоторая подготовка к дефердам.

      Удалить