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

    В предыдущей статье были описаны основные принципы работы 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 можно еще очень долго, в качестве дополнительного чтения могу опять порекомендовать список материалов в конце предыдущей статьи.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 18

      +2
      Спасибо, хотелось бы больше слышать о twisted:))
        +2
        Следующий пост про Twisted будет, обещаю! )
        0
        Огромное спасибо за статью. Продолжайте писать. Очень интересна реализация Deferred. А вообще, в Java подобное уже реализовано?
          0
          Насчет Java ничего не знаю, к сожалению… Twisted, насколько я знаю, близок к тому, чтобы работать под Jython, т.е. в каком-то плане может быть доступно.

          Самая большая проблема для статически типизированных языков — построение цепочки callbackов, на C++ в реализации самая большая проблема именно с этим, там жуткие шаблоны ;)
          0
          А вы не пробовали запускать написанное на twisted на шаред или вдс хостингах?
            0
            Ох, не силён я в терминологиях хостинговых )

            По сути, twisted-приложение — это обычное питоновское приложение. Если есть python, есть возможность запускать процессы «в фоне» (есть шелл), есть возможность установить питоновский пакет (twisted), есть возможность забиндиться на порт и т.п., то приложение будет работать. Для этого нужен выделенный сервер или виртуальный выделенный сервер (т.е. наличие своего IP, шелла и т.п.). Рутовый доступ как таковой не нужен, если есть Python, все пакеты можно установить и в свой домашний каталог.
              0
              Я пытался запустить, но висло все именно на socket.bind.
              Приложение автоматически биндилось на странный ип (типа 127.0.4.x) и результат отправленного запроса просто не приходил обратно в функцию, все умирало по таймауту. У сервера есть конечно внешний ИП, но на него биндиться не получилось :(

              Думал, может у кого есть опыт, хотел джаббер бота запустить на хостинге :)
                0
                По умолчанию Twisted биндится на все интерфейсы, возможно это у него не получается, и надо ему сказать выбирать конкретный адрес (интерфейс). Ну и на всякий случай не-рутовые приложения не могут получить порт ниже 1024.
                  0
                  Работают боты на vps с twisted, все отлично:)
                    0
                    До этого был на freebsd там так и не смог победить проблемы с kqueue, суть в том что если я использовал потоки twisted начинал юзать обычный селект, перешел на debian там с epoll проблем нет:)
                      0
                      Там с kqueue надо было патчить порт py-kqueue, после этого счастье ;) А еще был баг в самом Twisted, но они это пофиксили в новых версиях.
                        0
                        Патчил, думаю проблема была не в этом:)
                          0
                          И еще был чудесный баг, twistd не запускал процесс в фоне с --reactor=kqueue. Помогал ключик '-n' и уход в демона внешними средствами.
                0
                Очеь интересная идея, не слышал о ней раньше, но сразу же появляются 2 вопроса:

                1) Там где есть асинхронный вызов процедур, нужен таймаут, иначе есть вероятность, что она так и не завершится, а в программе будет странный непонятный глюк. Соответственно имеет наверно смысл и ограничение общего кол-ва «отложенных» процедур.
                2) Ну и поддерживать такие вещи надо на уровне компилятора и машинного кода, так как вся эта конструкция наверно несет оверхед.
                  0
                  1. Таймаут нужен там, где он может произойти, ну, например, вызов connect(). Если он завершится ошибкой, надо в цепочку Deferred запустить Failure, т.е. обычное исключение. Таймаут не отличается от разрыва соединения и т.п. Twisted это умеет.
                  2. Не несет существенного, она очень-очень простая. Полторы страницы кода (Python). Научиться ей пользоваться и думать в её терминах сложнее, но реализация простая и эффективная.
                  0
                  А возможно ли на Deferred сделать обработку исключений?

                  Например (возьмём первый рисунок), при выполнении без ошибок: callback2 выделила некоторый ресурс (который обязательно необходимо освободить), callback3 использовала этот ресурс, и callback4 его освободила. В какой errback надо установить код освобождения этого ресурса если в callback3 возникнет исключение? Напрашивающийся ответ — errback4, но в этом случае он так же будет вызван если ошибка возникнет в callback1, ещё до выделения ресурса, что неприемлемо.
                    0
                    Лучше смотреть на картинку «Deferred в квадрате», и рассматривать Deferred2 как отдельный поток, который начинается выделением ресурса (т.е. к моменту прихода на уровень callback2_1 ресурс уже выделен).

                    У Deferred есть метод .addBoth, который добавляет один и тот же обработчик на оба слота (callback/errback) на одном уровне. Это модель try...finally, который и используется для гарантированного освобождения ресурса при любом исходе.
                    0
                    Спасибо, очень полезно и как раз в теме на работе — разбираюсь с оптимизацией доступа к бд

                    Only users with full accounts can post comments. Log in, please.