18 марта 2015 в 00:57

Имплементация coroutine в NodeJS

Выход iojs сподвиг меня на изучение функций, которые уже стали стабильными в v8, в частности нативным промисам и генераторам, которые можно превращать в корутины. Удивился, что на Хабре нету статьи посвященной тому как самому сделать еще одну имплементацию coroutine через генераторы и понять, что же на самом деле происходит в co/bluebird. Чтобы начать пользоваться без страха перед магией coroutine прошу под кат.


Корутины(coroutine) — это функция(программа) имеющая несколько входных точек. Ее можно остановить в определенной точке, выполнить что-то другое, а затем снова вернуться в эту точку и продолжить с новыми данными(необязательно с новыми данными но с тем же состоянием, что и было). Fiber является одной из имплементацией корутин для nodejs(например node-fibers), но сейчас мы говорим об имплементации без написания плагина на C++ или метапрограммирования, только нативными методами Javascript.


В частности корутины нам дают возможность писать подобный код:
function timeout(ms, msg){
    return new Promise(function(resolve, reject){
        setTimeout(function(msg){
            console.log(msg);
            resolve(msg);
        }, ms, msg);
    }); 
}
 
var makeJob = function *(){
    yield timeout(1000, 1);
    yield timeout(1000, 2);
    var user = yield getUser();
    return user.info;
};
generatorWrapper(makeJob);


Получается довольно кратко и понятнее. Вместо timeout может быть любая другая функция умеющая в promise'ы, например ходящая за данными в базу и возвращающая данные прямо в эту же функцию, а не в then/callback, что довольно удобно, т.к. позволяет и отменять исполнение последовательных асинхронных функций более удобно, чем это возможно в promise chain.
Вся суть вопроса как выглядит этот generatorWrapper. И это мои рассуждения на этот счет.
function async(gen){
    var instance = gen(); //создаем инстанс генератора, по которому будем итерировать
    return new Promise(function(resolve, reject){
//функция обертка для приходящих данных из корутины
        function next(r){
            if (r.done)
            {
                resolve(r.value);//если все закончилось просто отдаем resolve'им результат генератора.
            }
            if (typeof(r.value.then)=='function')//проверка через duck typing, т.к. есть много Promise совместимых библиотек
            {
                r.value.then(function(someRes){
                    next(instance.next());//вызываем next снова на новых данных.
                }, function(e){
                    reject(e);
                });
            }
            else
            {
                console.log(r.value);
                next(instance.next());
            }
        }
        next(instance.next());//отдаем результат из генератора в функцию обертка
    });
}

Эта имплементация идеологическая для понимания как оно все работает, но идеология примерно такая же в co и bluebird.
Конечно полноценными имплементациями является co и bluebird.coroutine, в них есть паралельная обработка и возможность расширенной обработки в том числе работа с другими генераторами.

По коду видно, что генератор в JS, хоть и позволяет писать код вида как корутины, но на самом деле не предназначен для этого. Генератор, извините за капитанство и тавтологию, прежде всего генератор и наиболее удобно на нем все таки делать ленивые вычисления, а использование его подобным образом не совсем его прямое назначение, хотя по моим тестам никаких деградации производительности не заметил.
С другой стороны становится ясно глядя на будущие стандарты типа await/async, что все в принципе не так уж далеко, уже текущими корутинами на базе генераторов можно спокойно пользоваться используя iojs + co/bluebird не опасаясь за стабильность продукта. А учитывая, что все правильные имплементации корутин возвращают нативный promise то все это вполне совместимо с грядущими стандартами.

Резюмирую: спокойно ставьте и используйте это в iojs. В код написанный на промисах все это встает без каких либо изменений. Если что-то хочется добавить к функционалу можете поглядеть в тот же co и дописать что-то свое, там нет ничего страшного в этой обработке. Если хочется сделать async race не отказывайте себе в этом, но придется тогда это делать в другой функции на promise'ах, не генераторе, или придумать свой формат данных, в остальном feel free с генераторами, они теперь такая же часть стандарта как и все остальное.

P.S. Извините за сырой материал, надеюсь на вашу помощь по его улучшению.
Мендяев Николай @KlonD90
карма
2,0
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (35)

  • +1
    Без сомнений, async/await очень хороши, хотя лично по моему мнению ключевое слово async излишне — мне кажется, что рай на земле наступил бы мгновенно после того, как все функции стали бы неявно async-овыми. В первую очередь я думаю о трапах Proxy — чтобы можно было сделать, скажем, proxy.foo = 123, и это вызвало асинхронный обработчик, который мог бы сколько угодно блокировать вызывающий контекст — пока сходит в интернет, пока скажет, что значение свойства foo обновилось, пока вернется обратно. Полная синхронная семантика.
    • 0
      промах
    • –1
      Вы, фактически, предлагаете сделать javascript многопоточным языком. Но тогда возникнут проблемы с синхронизацией потоков — если любое присваивание может оказаться асинхронным и переключить контекст исполнения — как вообще писать алгоритмы?

      Нет, я понимаю, что такие способы есть — пишут же как-то на плюсах :) Но именно однопоточность javascript — ключевая характеристика языка.
      • –1
        Нет, асинхронность не подразумевает многопоточности. Генераторы ведь не создают многопоточность. Я прошу о мелочи — об изменении семантики доступа к аттрибутам на асинхронную :) Т.е. чтобы присваивания вида foo.bar = 123 были типа как await foo.setAttr('bar', 123). Ну или, если более многословно и не так корректно, foo.setAttr('bar', 123, (function() { продолжение функции }).bind(this)).
        • 0
          Чем тотальная неявная асинхронность отличается от многопоточности на одноядерном процессоре?
          Гонки и ошибки синхронизации вполне себе возможны и на одном ядре.
          • –4
            1. откуда берется неявность? Многопоточность — это вытесняющая многозадачность. Асинхронность — это кооперативная многозадачность. В первом случае поток прерывает свое выполнение неявно, в любой момент, который покажется удобным планировщику. Во втором случае поток явно возвращает управление планировщику — в моем случае при обращении к какому-либо аттрибуту.
            2. подскажите, какие могут быть «ошибки синхронизации», когда у вас физически один поток? Т.е. не много потоков на одноядерном процессоре, как вы подразумеваете, а один поток с одной очередью сообщений, как это происходит в JS. Правильный ответ — никаких race conditions и ошибок синхронизации быть не может принципиально, по той же причине, по которой их не может быть в coroutines.
            • +2
              Ну вот запустили вы код:
              x.alpha += x.beta
              

              Это *условно* эквивалентно чему-то вроде
              x.getAttr('alpha', function(alpha) {    // f1
                  y.getAttr('beta', function(beta) {  // f2
                     x.setAttr('alpha', alpha + beta, function() {
                       // do nothing
                     }
                  }
                }
              


              Одновременно с этим кодом некто запустил
              x.alpha -= x.gamma
              

              Что можно записать как
              x.getAttr('alpha', function(alpha) {      // f3
                  y.getAttr('gamma', function(gamma) {  // f4
                     x.setAttr('alpha', alpha - gamma, function() {
                       // do nothing
                     }
                  }
                }
              

              А теперь вопросы на засыпку.

              В каком порядке функции f1, f2, f3, f4 могут выполниться?
              Будет ли результат их выполнения зависеть от порядка?
              Каков будет результат, если порядок вызова будет f1 -> f3 -> f2 -> f4?
              Как по-умному называются такие ошибки (подсказка: race...)?

    • 0
      это же хорошо, быть уверенным, что между соседними операциями не вклинится еще чего, а то вдруг пока мы будем ждать, где-нибудь этот самый proxy удалится
      • +1
        Ну да, однопоточная природа JS гарантирует, что никто между соседними операциями не вклинится таким образом, чтобы proxy взял и удалился. Потому что, как минимум, на него есть ссылка в контексте функции, которая ждет завершения асинхронного коллбэка. А так, чтобы просто взял и вклинился и сделал что-то свое — ну так в этом вся суть асинхронного программирования. Чтобы не кто-то один в одно рыло все делал, а каждый, когда появляется возможность, брал и вклинивался максимально детерминированным способом. Почему подобные аргументы не применяются по отношению к коллбэкам, промисам или генераторам? Там везде все постоянно вклиниваются, и объекты из-за этого меняются, и в этом и есть весь смысл.

        Возможно, у вас могло сложиться впечатление, что «вклинивание» происходит недетерминированно, сродни вытесняющей многозадачности. Нет. Присванивание поля вызывает трап прокси, который является обыкновенной функцией, и которая затем должна вернуть значение. Уже сейчас можно вклиниться в процесс обычного присвания. Я лишь предлагаю сделать еще один шаг и сделать допустимым асинхронную обработку трапов в духе современного JS.
        • 0
          Почему подобные аргументы не применяются по отношению к коллбэкам, промисам или генераторам?
          потому что все явно, это ожидаемое поведение
          • +1
            Ну так и тут все явно. Где неявность-то, объясните слепому? Почему __setattr__ в Python — это явно, а set trap в ES6 Proxy — неявно? Почему промис — это явно, а тот же промис, но вызванный в обработчике события — это неявно? Глупости и ложные выводы, напрямую следующие из непонимания основополагающих принципов Javascript.
            • 0
              Почему __setattr__ в Python — это явно, а set trap в ES6 Proxy — неявно?
              не знаю, не достаточно в теме
              вы хотите, чтобы асинхронный код был неотличим от синхронного, и вообще, пример с сеттером показывает сопрограммы как-то не с той стороны
              основополагающих принципов Javascript
              принципы в студию

              Уже сейчас можно вклиниться в процесс обычного присвания.
              Как именно? Думал, что исполняемый кусок кода не прерывается, а события обрабатываются, когда никакой другой код не исполняется. Именно это подразумевал под ожидаемым поведением. А если в каком-то месте ожидание события, то значит, там могут обработаться и другие события(собственно без этого нет и плюсов по сравнению с обычным синхронным вариантом), то есть нарушается атомарность(как будто все выполняется в критических секциях).
    • 0
      Если бы все функции были бы асинхронными сильно ухудшилось бы применение NodeJS как script'ового языка именно для решения каких-то задач, где и нужна асинхронность, но на самом деле она нужна ровно в одном месте, где-то где перемалываются большие куски данных или происходит какая-то важная асинхронная часть. С моей точки зрения сейчас все NodeJS модули вместо калбэков должны уметь в Promise, потому что стандартом стало уже написание на промисах + добавление больше синхронных функций, чтобы проще было писать код для тулзов. Впрочем внедрение await async уберет эти проблемы при написание тулзов.
      Хотя если копать глубже то такой подход на самом деле используется только на уровне NodeJS, которая и делает асинхронное взаимодействие, но не через паузу content'а, а через eventloop. Получается сама NodeJS успела устареть и не поспевает за скоростью разработки v8 js'а. Т.е. в принципе в этой ситуации понятно, что NodeJS играет роль догоняющего, фактически генераторы дают этакую паузу контента уже сейчас осталось только это где-то внедрить по грамотнее, наверное тоже ожидают, что на основе генераторах сделают async/await.

      В принципе update'ить структуру данных можно, только тогда set'еры должны возвращать Promise'ы. Плюс можно сделать что-то типа lazy getter'ов, которые бы тоже ходили куда-то или не ходили но в любом случае вернули Promise, в принципе для этого конечно больше подходят future'ы тогда, но пока они не реализованы в принципе можно и по promise'ам резолвить. В принципе если все по итогу будет возвращать Promise'ы то станет гораздо легче, нужно только выработать какой-то стандарт отлова синхронных ошибок, т.к. в каждом промисе нужно писать еще try{ } catch{} и catch для ошибок пришедших из предыдущего Promise'а.
      • 0
        Ну вот я о том и говорю, чтобы фактически всегда возвращать промисы, принимать промисы, и вообще, сделать так, что любая функция всегда возвращает промис — а уж когда она вернет настоящее значение и зарезолвит промис, который она вернула сразу — дело времени. И делать все это, естественно, автомагически.
        • 0
          А вы в продакшене уже используете генераторы или es7 transpiler? Я пока опасался за рост стэка, но потом понял, что через Promise'ы event loop stack не растет, раньше пользовался iced-coffeescript, но там опыт был печальный из-за довольно грустной на те времена реализации подобного подхода, так что я пока опасался. Впрочем код написанный с es7 transpiler все равно меня пугает, но co вполне приятный подход дает.
          • 0
            Нет, к сожалению, но собираюсь наконец-то перебороть отвращение к транспилерам и попробовать. Какой порекомендуете из хороших в плане качества выхлопа?
            • 0
              babel
  • +2
    :) Уже давно клинки сточили о «тяжелые сеттеры», а вы ещё предлагаете асинхронные действия проводить. Рай, он может так со стороны кажеться, а оказавшись там — увидите, что код станет абсолютно не предсказуем. Явное лучше неявного, особенно в js.
    • +1
      вы ещё предлагаете асинхронные действия проводить

      А вы предлагаете все действия проводить синхронно, блокируя поток в однопоточном JS? :)

      Рай, он может так со стороны кажеться, а оказавшись там — увидите, что код станет абсолютно не предсказуем.

      Вы считаете лапшу из коллбэков более предсказуемой, чем async / await и обещания? Сразу видно, что это вы смотрите со стороны. Предположим, десяток последовательных асинхронных задач, необработанная ошибка в одном коллбэке — и всё перестаёт работать, когда с async / await достаточно всё обернуть в один try / catch.
      • +1
        Похоже вышло недопонимание.
        > А вы предлагаете все действия проводить синхронно, блокируя поток в однопоточном JS? :)
        Я предлагаю совсем не проводить никаких тяжёлых действий в сеттерах.

        > Вы считаете лапшу из коллбэков более предсказуемой, чем async / await и обещания?
        Между каких строк вы это увидели? :). Совсем нет, но автор комментария предлагает вовсе отказаться от ключевых слов — async/await, и в дополнение речь шла не только о асинхронных функциях, но и сеттерах.
        • 0
          И как это, интересно, я мог не догадаться, что ваш комментарий — ответ на чью-то реплику о прокси? Может потому, что ваш комментарий — комментарий к статье, и никак на ту реплику не ссылается? :)
          • 0
            Тьфу, точно :) Комментарий не успел отредактировать. Простите за путаницу.
    • 0
      Категорически не согласен. Например, мы пишем красивый удобный freestyle ORB со следующим интерфейсом — сервер расшаривает какой-то объект, а клиенты обращаются к этому объекту так, как будто бы он локальный. Т.е. вызывают у него методы, читают свойства, пишут аттрибуты, итерируются по нему, вызывают его в явном виде, если он функция, делают на нем new и т.д. Как этого достичь?

      Когда любой объект нужно передать куда-нибудь по сети, мы назначаем этому объекту новый UUID, предварительно проверив в локальной слабой карте, что для этого объекта UUID еще нет. И вместо объекта, отправляем получателю только UUID. На принимающей стороне UUID заменяется на ES6 Proxy с вшитым идентификатором, любой чих отправляющий владельцу объекта. Подвох в том, что у того же трапа get вот такая сигнатура — (target, name, proxy): value. Т.е. ответ от нас ожидается сразу же, но у нас его нет — он где-то на другом компьютере, далеко. Есть три варианта:
      1. забыть, понять и простить. Плюс — просто в реализации, просто не нужно ничего делать. Минус — ничего не работает.
      2. синхронный XMLHttpRequest. Плюс — работает. Минус — лучи мягкого стула от других разработчиков за блокирование очереди событий.
      3. использовать асинхронную семантику трапов. Плюс — то, что нужно. Минус — пока ES7 не стал стандартом, не понятно, будет ли такое возможно.

      В третьем, последнем случае, если бы async/await еще не существовало, сигнатура могла бы вместо value возвращать Promise, а реализация бы просто не возвращалась к месту сработки трапа, пока Promise не зарезолвится. Но если функцию трапа можно будет объявить, как async, то это тоже будет хорошо.

      Если вы знаете, как другим способом сделать подобное — буду рад услышать. Из того, что мне приходило в голову альтернативного — отказ от несериализуемых объектов, конвертация всего в JSON, отказ от геттеров-сеттеров и оперирование исключительно функциями, исключительно асинхронными, принимающими коллбэк в качестве параметра. Ну а это уже совсем не красивый и элегантный ORB, а dnode какой-то, простите.
      • 0
        мне кажется, вы хотите переизобрести DCOM только для js
        как насчет варианта синхронный XMLHttpRequest в worker?
        • 0
          Ну, вообще нет — из-за отсутствия интерфейса объектов. С другой стороны, если представить proxy в виде жесткого интерфейса, то очень похоже, т.к. я COM-ом вдохновлялся. Мне показалось элегантным решение без объявления вообще чего бы то ни было и, что важно, без необходимости сериализовывать что-либо, отличное от примитивных типов.

          Синхронный XHR в воркере не сработает по той причине, что с воркером нужно общаться — а с ним можно общаться только асинхронно через postmessage. Шило на мыло, увы. Либо синхронный XHR (медленно и печально), либо асинхронные трапы (если они будут сделаны), тут иначе никак, увы.
          • 0
            что скажете по поводу добавления уровня абстракции. создаем очередь из действий и ожидаемых событий, аналог expect:
            spawn()
            .send(target, 'insert', {id: value})
            .do(log("inserting..."))
            .expect(target, 'inserted', function(result){log(result)})
            .do(log("inserted."))
            .wait(100)
            .start()
            
            запускаем несколько таких очередей, чем не green threads
      • 0
        Я понимаю, что вы хотите сделать, но вы через чур вдаетесь в детали — нужно подняться немного выше)
        — Сеттеры/Геттеры — по моему опыту, они должны быть очень легкими, лишь манипуляция полями своего объекта и отсылка событий, в противном случае код и архитектура становятся запутанными и не явными.
        — Методы — вот здесь имеет место разговор о асинхронности, но что при этом выполняется абсолютно не важно, потому что абстракции предполагают что мы можем, например, использовать разный транспорт для сохранения объекта. И не важно будь то удалённый вызов процедур, запись в локальную базу или ещё что-то там.

        Как для меня, самым лучшим вариантом является ваш третий вариант, но модифицирован — Promise но с async/await. Также как сейчас в C# Task<T> var user = await Service.Fetch(params);. ES7 движется в этом направлении, и с traceur уже можно с этим играться.
  • 0
    Библиотека для coroutine ruff делает то же, что и Co. Только проще: без промисов, но с ивентами.
    А еще она работает в браузере без необходимости использовать browserify.
    • 0
      В NodeJS конкретно эту бы имплементацию вам бы не советовал бы использовать. Она не использует EventEmitter. Но для браузера наверное нормально.
      • 0
        Что плохого в том, что эта имплементация не использует EventEmitter?
        • 0
          Ну там костыль со всеми вытекающими. Нельзя отписаться от event'а или перезаписать его и черт его знает как у его имплементации с оптимизацией, уже вижу что он использует forEach, что довольно неплохой удар по оптимизиации. Все таки пользоваться стандартной библиотекой для бэкэнда имеет больше смысла. Для фронта конечно ok.
          • 0
            Пожалуй вы правы, в версии 1.3.0 учтены ваши замечения.
  • 0
    <shameless-plug>
    А я давно уже такое использую: asjs

    Ну и в разных популярных препроцессорах уже появилось, так что мне непонятно, зачем люди всё ещё лепят лапшу из колбэков и then-ов…
    • +1
      А не могли бы вы пояснить что в каких препроцессорах уже появилось?
      • 0
        И babel, и traceur прекрасно поддерживают async / await. Нужно что-то ещё? :)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.