Основная цель написания сего поста: разобраться в том, как работает декоратор @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.
В "поджарке" также будут участвовать:
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.
Мне непонятно, как из описанных в начале ингридиентов появился шашлык из свинины?
ОтветитьУдалитьПожарили деферды -- получили шашлык из свинины. Ты недостаточно абстрагировалась!
Удалить>> Внешний цикл не должен прерываться ... А наши задачи не должны быть блокирующими
ОтветитьУдалить>> 5. Когда наши колбеки выполняются, цикл реактора не выполняется, и наоборот.
я ничего не понял х)
Реактор работает в одном потоке. Он ждет событие. К реактору в гости стучится некоторое событие. Реактор открывает дверь и одновременно, никакого другого действия не может выполнять. Тут наше событие прям сразу засыпает при реакторе. Реактор не понимает, что ему делать с таким событием и тоже засыпает. "Спячка" в данном случае -- блокирующее действие (наподобие sleep()).
УдалитьЦикл реактора выполняется -- это означает, что реактор готов принимать некоторые события.
И что такое run_print() :)
ОтветитьУдалитьrun_print() -- это неведомая функция, которая будет просто что-то печатать. В синхронном коде она не принимает ничего, в асинхронном -- внутрь нее должен передаваться коллбек и еррбек. Ну это некоторая подготовка к дефердам.
Удалить