Pull to refresh

Deferred: все подробности

Reading time5 min
Views14K
В предыдущей статье были описаны основные принципы работы Deferred и его применение в асинхронном программировании. Сегодня мы постараемся рассмотреть в деталях функционирование Deferred и примеры его использования.

Итак, Deferred — это отложенный результат, результат выполнения, который станет известен через некоторое время. Результатом, хранящимся в Deferred, может быть произвольное значение (успешное выполнение) или ошибка (исключение), которое произошло в процессе выполнения асинхронной операции. Раз нас интересует результат операции и мы получили от некоторой асинхронной функции Deferred, мы хотим выполнить действия в тот момент, когда результат выполнения будет известен. Поэтому Deferred кроме результата хранит еще цепочку обработчиков: обработчиков результатов (callback) и обработчиков ошибок (errback).

Рассмотрим поподробнее цепочку обработчиков:

Deferred

Обработчики располагаются по “слоям” или уровням, выполнение происходит четко по уровням сверху вниз. При этом на каждом уровне расположены обработчики callback и errback, один из элементов может отсутствовать. На каждом уровне может быть выполнен либо callback, либо errback, но не оба. Выполнение обработчиков происходит только один раз, повторного входа быть не может.

Функции-обработчики callback являются функциями с одним аргументом — результатом выполнения:

def callback(result):
    …

Функции-обработчики errback принимают в качестве параметра исключение, “завернутое” в класс Failure:

def errback(failure):
    …

Выполнение Deferred начинается с того, что в Deferred появляется результат: успешное выполнение или исключение. В зависимости от результата выбирается соответствующая ветвь обработчиков: callback или errback. После этого происходит поиск ближайшего уровня обработчиков, в котором существует соответствующий обработчик. В нашем примере на рисунке был получен успешный результат выполнения и результат был передан обработчику callback1.

Дальнейшее выполнение приводит к вызову обработчиков на нижележащих уровнях. Если callback или errback завершается возвратом значения, которое не является Failure, выполнение считается успешным и полученный результат отправляется на вход callback-обработчику на следующем уровне. Если же в процессе выполнения обработчика было выкинуто исключение или возвращено значение типа Failure, управление будет передано errback на следующем уровне, который получит исключение в качестве параметра.

В нашем примере обработчик callback1 выполнился успешно, его результат был передан обработчику callback2, в котором было выкинуто исключение, которое привело к переходу к цепочке errback-обработчиков, на третьем уровне обработчик errback отсутствовал и исключение было передано в errback4, который обработал исключение, вернул успешный результат выполнения, который теперь является результатом Deferred, однако больше обработчиков нет. Если к Deferred будет добавлен еще один уровень обработчиков, они смогут получить доступ к этому результату.

Как и все другие объекты Python, объект Deferred живет до тех пор, пока на него есть ссылки из других объектов. Обычно объект, вернувший Deferred, сохраняет его, т.к. ему надо по завершении асинхронной операции передать в Deferred полученный результат. Чаще всего другие участники (добавляющие обработчики событий) не сохраняют ссылки на Deferred, таким образом объект Deferred будет уничтожен по окончании цепочки обработчиков. Если происходит уничтожение Deferred, в котором осталось необработанное исключение (выполнение завершилось исключением и больше обработчиков нет), на экран печатается отладочное сообщение с traceback исключения. Эта ситуация аналогична “выскакиванию” необработанного исключения на верхний уровень в обычной синхронной программе.

Deferred в квадрате


Возвращаемым значением callback и errback может быть также другой Deferred, тогда выполнение цепочки обработчиков текущего Deferred приостанавливается до окончания цепочки обработчиков вложенного Deferred.

deferred-in-deferred

В приведенном на рисунке примере обработчик callback2 возвращает не обычный результат, а другой Deferred — Deferred2. При этом выполнение текущего Deferred приостанавливается до получения результата выполнения Deferred2. Результат Deferred2 — успешный или исключение — становится результатом, передаваемым на следующий уровень обработчиков первого Deferred. В нашем примере Deferred2 завершился с исключением, которое будет передано на вход обработчику errback2 первого Deferred.

Обработка исключений в errback


Каждый обработчик исключений errback является аналогом блока try..except, а блок except обычно реагирует на некоторый тип исключений, такое поведение очень просто воспроизвести с помощью Failure:

def errback(failure):
    """
    @param failure: ошибка (исключение), завернутое в failure
    @type failure: C{Failure}
    """
    failure.trap(KeyError)

    print "Got key error: %r" % failure.value

    return 0

Метод trap класса Failure проверяет, является ли завернутое в него исключение наследником или непосредственно классом KeyError. Если это не так, оригинальное исключение снова выбрасывается, прерывая выполнение текущего errback, что приведет к передаче управления следующему errback в цепочке обработчиков, что имитирует поведение блока except при несоответствии типа исключения (управление передается следующему блоку). В свойстве value хранится оригинальное исключение, которое можно использовать для получения дополнительной информации об ошибке.

Необходимо обратить внимание, что обработчик errback должен завершиться одним из двух способов:
  1. Вернуть некоторое значение, которое станет входным значением следующего callback, что по смыслу означает, что исключение было обработано.
  2. Выкинуть оригинальное или новое исключение — исключение не было обработано или было перевыброшено новое исключение, цепочка обработчиков errback продолжается.


Существует и третий вариант — вернуть Deferred, тогда дальнейшее выполнение обработчиков будет зависеть от результата Deferred.

В нашем примере мы исключение обработали и передали в качестве результата 0 (например, отсутствие некоторого ключа эквивалентно его нулевому значению).

Готовимся к асинхронности заранее


Как только появляется асинхронность, то есть некоторая функция вместо непосредственного значения возвращает Deferred, асинхронность начинает распространяться по дереву функций выше, заставляя возвращать Deferred из функций, которые раньше были синхронными (возвращали результат непосредственно). Рассмотрим условный пример такого превращения:

def f():
    return 33

def g():
    return f()*2

Если по каким-то причинам функция f не сможет вернуть результат сразу, она начнет возвращать Deferred:

def f():
    return Deferred().callback(33)

Но теперь и функция g вынуждена возращать Deferred, зацепляясь за цепочку обработчиков:

def g():
    return f().addCallback(lambda result: result*2)

Аналогичная схема “превращения” происходит и с реальными функциями: мы получаем результаты в виде Deferred от нижележащих в дереве вызовов функции, навешиваем на их Deferred свои обработчики callback, которые соответствуют старому, синхронному коду нашей функции, если у нас были обработчики исключений, добавляются и обработчики errback.

На практике лучше сначала выявить те места кода, которые будут асинхронными и будут использовать Deferred, чем переделывать синхронный код в асинхронный. Асинхронный код начинается с тех вызовов, которые не могут построить результат непосредственно:
  • сетевой ввод-вывод;
  • обращение к сетевым сервисам СУБД, memcached;
  • удаленные вызовы RPC;
  • операции, выполнение которых будет выделено в нить в модели Worker и т.п.

В процессе написания приложения часто ясно, что в данной точке будет асинхронное обращение, но его еще нет (не реализован интерфейс с СУБД, например). В такой ситуации можно использовать функции defer.success или defer.fail для создания Deferred, в котором уже содержится результат. Вот как можно короче переписать функцию f:

from twisted.internet import defer

def f():
    return defer.success(33)

Если мы не знаем, будет ли вызываемая функция возвращать результат синхронно или вернет Deferred, и не хотим зависеть от её поведения, мы можем завернуть обращение к ней в defer.maybeDeferred, которое любой вариант сделает эквивалентным вызову Deferred:

from twisted.internet import defer

def g():
    return defer.maybeDeferred(f).addCallback(lambda result: result*2)


Такой вариант функции g будет работать как с синхронной, так и с асинхронной f.

Вместо заключения


Рассказывать о Deferred можно еще очень долго, в качестве дополнительного чтения могу опять порекомендовать список материалов в конце предыдущей статьи.
Tags:
Hubs:
Total votes 18: ↑18 and ↓0+18
Comments18

Articles