Такая разная асинхронность

    Здравствуйте, меня зовут Дмитрий Карловский и я… многозадачный человек. В смысле у меня много задач и мало времени, чтобы их все уже, наконец, закончить. Отчасти это и к лучшему — всегда есть чем заняться. С другой стороны — пока ты разрываешься между проектами, мир катится куда-то не туда и некому забраться на броневик и призвать толпу остановиться и немного подумать. А вопрос-то серьёзный — долгое время мир JS был погружён в ад обратных звонков и с ними не только не боролись — их боготворили. Потом он чуть менее чем полностью погряз в обещаниях. Сейчас к ним с разных сторон усиленно вставляют подпорки разной степени кривизны. А света в конце тоннеля всё не видать. Но обо всём по порядку...


    Теория многозадачности


    Сперва определимся с терминами. В процессе работы, приложение выполняет различные задачи. Например, "скачать файл с удалённого сервера" или "обработать запрос пользователя".


    Не редки ситуации, когда для выполнения одной задачи требуется выполнение дополнительных задач — "подзадач". Например, для обработки запроса пользователя, необходимо скачать файл с удалённого сервера.


    Запустить подзадачу мы можем синхронно, и тогда текущая задача заблокируется в ожидании завершения подзадачи. А можем запустить асинхронно, и тогда текущая задача продолжит своё выполнение не дожидаясь завершения подзадачи.


    Тем не менее, обычно для завершения выполнения задачи, пусть и не сразу, но требуется и завершение выполнения подзадачи с последующей обработкой её результатов. Блокировку одной задачи в ожидании сигналов от другой будем называть "синхронизацией". В общем случае, синхронизация одних и тех же задач может происходить и множество раз, по самой различной логике, но в дальнейшем мы будем рассматривать лишь простейший и самый распространённый вариант — синхронизацию по завершению подзадачи.


    В языках, поддерживающих многопоточность, обычно каждая задача запускается в отдельном "системном потоке" или (более правильно) "нити". Каждая нить может исполняться на отдельном ядре процессора, параллельно с другими нитями. Так как нитей может быть много, а число ядер весьма ограничено, то операционная система реализует механизм "вытесняющей многопоточности", когда любая нить, если она долго исполняется, может быть принудительно приостановлена, чтобы дать возможность поработать другим нитям.


    Параллельная работа задач приводит к различным проблемам при работе с общей памятью, для решения которых приходится использовать нетривиальные механизмы синхронизации. Чтобы упростить работу программиста и повысить надёжность, производимого им программного обеспечения, некоторые языки полностью отказываются от многопоточности и запускают все задачи в одной единственной нити. Многозадачность в этом случае реализуется одним из следующих способов:


    Волокна (fibers), также известные как "сопрограммы" (coroutines). По сути это те же нити, но реализующие "кооперативную многозадачность". Все волокна имеют свои стеки, но исполняются в рамках одной нити, а значит не могут исполняться параллельно. При этом решение о том, когда переключить нить на другое волокно, принимает само волокно.


    Цепочки задач. Суть подхода в том, что вместо того, чтобы приостанавливать текущую задачу на время выполнения подзадачи, мы разбиваем задачу на много маленьких подзадач и говорим каждой, какую подзадачу нужно выполнить по завершении этой.


    Конечные автоматы (state machine), также известные как "генераторы" (generators), "асинхронные функции" (async functions) и "полусопрограммы" (semicoroutines) и "сопрограммы без стека" (stackless coroutines). Фактически, это объекты, хранящие локальное состояние единственного метода, в начале которого находится ветвление с переходом к коду одного из шагов исходной задачи. По завершении шага управление возвращается вызвавшей функции. Повторный вызов асинхронной функции уже приводит к переходу к другому шагу.


    Реализации на NodeJS


    В репозитории nin-jin/async-js в отдельных ветках собраны реализации простого приложения на разных моделях многозадачности. Суть приложения простая и состоит из 3 частей:


    1. Модель (user.js). Загружает конфиг с диска и предоставляет метод для получения имени пользователя из этого конфига.
    2. Отображение (greeter.js). Принимает модель пользователя и печатает, обращение к нему в консоль.
    3. Контроллер (index.js). Печатает пользователю приветствие, а затем прощание. Попутно выводит время своей работы и логирует ошибку, если происходит исключительная ситуация, не давая процессу упасть.

    Конфиг простой:


    {
        "name" : "Anonymous"
    }

    Синхронный код


    user.js


    var fs = require( 'fs' )
    
    var config
    
    var getConfig = () => {
        if( config ) return config
    
        var configText = fs.readFileSync( 'config.json' )
        return config = JSON.parse( configText )
    }
    
    module.exports.getName = () => {
        return getConfig().name
    }

    greeter.js


    module.exports.say = ( greeting , user ) => {
        console.log( greeting + ', ' + user.getName() + '!' )
    }

    index.js


    var user = require( './user' )
    var greeter = require( './greeter' )
    
    try {
    
        console.time( 'time' )
        greeter.say( 'Hello' , user )
        greeter.say( 'Bye' , user )
        console.timeEnd( 'time' )
    
    } catch( error ) {
        console.error( error )
    }

    Крайне простой и понятный. В нём легко разбираться и не менее легко вносить изменения. Но у него есть один существенный недостаток — пока выполняется эта задача никакая другая задача выполнена быть не может, даже если мы ждём загрузки файла с сетевого диска и ничего полезного не делаем. Если это скрипт одной задачи, как в примере выше, то ничего страшного, но если нам нужен веб-сервер, который должен обрабатывать множество запросов одновременно, то однозадачное решение нам не подходит.


    Предопределённые цепочки


    Многие синхронные методы в NodeJS API имеют и свои асинхронные аналоги, где последним аргументом передаётся "продолжение" (continuation), то есть функция, которую следует вызвать после завершения асинхронной задачи.


    user.js


    var fs = require( 'fs' )
    
    var config
    
    var getConfig = done => {
    
        if( config ) return setImmediate( () => {
            return done( null , config )
        })
    
        fs.readFile( 'config.json' , ( error , configText ) => {
            if( error ) return done( error )
    
            try {
                config = JSON.parse( configText )
            } catch( error ) {
                return done( error )
            }
    
            return done( null , config )
        })
    
    }
    
    module.exports.getName = done => {
    
        getConfig( ( error , config ) => {
            if( error ) return done( error )
    
            try {
                var name = config.name
            } catch( error ) {
                return done( error )
            }
    
            return done( null , name )
        } )
    
    }

    greeter.js


    module.exports.say = ( greeting , user , done ) => {
    
        user.getName( ( error , name ) => {
            if( error ) return done( error )
    
            console.log( greeting + ', ' + name + '!' )
    
            return done()
        })
    
    }

    index.js


    var user = require( './user' )
    var greeter = require( './greeter' )
    
    var script = done => {
        console.time( 'time' )
    
        greeter.say( 'Hello' , user , error => {
            if( error ) return done( error )
    
            greeter.say( 'Bye' , user , error => {
                if( error ) return done( error )
    
                console.timeEnd( 'time' )
    
                done()
            } )
    
        } )
    
    }
    
    script( error => {
        if( !error ) return
    
        console.error( error )
    } )

    Как видно, код заметно усложнился. Нам пришлось все (даже синхронные) функции, переписать в цепочечном стиле. При этом, правильная обработка ошибок доставляет особую боль: если забыть где-то обработать ошибку, то приложение может упасть, а может не упасть, а может упасть, но не сразу, а чуть позже, вдалеке от места возникновения ошибки. А если оно и каким-то чудом не упадёт, то и ошибка никаким образом залогирована не будет. Написание кода в таком стиле требует от программиста чуткости и внимательности, поэтому большинство модулей в NPM — заряженные пистолеты, способные в любой момент подарить вам незабываемые часы в компании отладчика.


    Постопредляемые цепочки


    Реализуемые через "обещания" (promises), они берут на себя основную работу по прокидыванию ошибок. Единственное, что нужно помнить — в конце цепочки должен стоять обработчик ошибок, иначе приложение может завершиться по среди выполнения задачи, ничего при этом не сказав.


    user.js


    var fs = require( 'fs' )
    
    var config
    
    var getConfig = () => {
        return new Promise( ( resolve , reject ) => {
            if( config ) return resolve( config )
    
            fs.readFile( 'config.json' , ( error , configText ) => {
                if( error ) return reject( error )
    
                return resolve( config = JSON.parse( configText ) )
            } )
        } )
    }
    
    module.exports.getName = () => {
        return getConfig().then( config => {
            return config.name
        } )
    }

    greeter.js


    module.exports.say = ( greeting , user ) => {
        return user.getName().then( name => {
            console.log( greeting + ', ' + name + '!' )
        } )
    }

    index.js


    var user = require( './user' )
    var greeter = require( './greeter' )
    
    Promise.resolve()
    .then( () => {
        console.time( 'time' )
        return greeter.say( 'Hello' , user )
    } )
    .then( () => {
        return greeter.say( 'Bye' , user )
    } )
    .then( () => {
        console.timeEnd( 'time' )
    } )
    .catch( error => {
        console.error( error )
    } )

    По сравнению с предопределёнными цепочками, код получился по проще, но всё так же разбит на множество мелких функций. Преимуществом данного подхода является то, что он будет работать одинаково хорошо в любом окружении. Даже там, где обещаний нет изначально — их легко добавить не сложной библиотекой.


    В целом, оба вида цепочек приводят к большому объёму визуального шума и усложнению написания нелинейных алгоритмов, использующих циклы, условные ветвления, локальные переменные и так далее.


    Генераторы


    Некоторые JS-движки поддерживают генераторы, которые довольно элегантно интегрируются с обещаниями, что позволяет реализовывать "приостанавливаемые функции" (awaitable).


    user.js


    var fs = require( 'fs' )
    var co = require( 'co' )
    
    var config
    
    var getConfig = () => {
        if( config ) return config
    
        return config = new Promise( ( resolve , reject ) => {
            fs.readFile( 'config.json' , ( error , configText ) => {
                if( error ) return reject( error )
                resolve( JSON.parse( configText ) )
            } )
        } )
    }
    
    module.exports.getName = co.wrap( function* () {
        return ( yield getConfig() ).name
    } )

    greeter.js


    var co = require( 'co' )
    
    module.exports.say = co.wrap( function* ( greeting , user ) {
        console.log( greeting + ', ' + ( yield user.getName() ) + '!' )
    } )

    index.js


    var co = require( 'co' )
    
    var user = require( './user' )
    var greeter = require( './greeter' )
    
    co( function*() {
        console.time( 'time' )
        yield greeter.say( 'Hello' , user )
        yield greeter.say( 'Bye' , user )
        console.timeEnd( 'time' )
    } ).catch( error => {
        console.error( error )
    } )

    Код получился почти столь же простым, что и синхронный, разве что нам пришлось все функции превратить в генераторы и завернуть в специальную обёртку, которая получив (yield) от генератора обещание, подписывается на его "резолв", после которого "продолжает" генератор с передачей ему полученного значения. Таким образом мы снова можем пользоваться условными ветвлениями, циклами и прочими идиомами управления потоком.


    Асинхронные функции


    Фактически это не более, чем синтаксический сахар для генераторов. Но сахар этот ещё мало где поддерживается, поэтому пока ещё приходится использовать babel для трансформации в код на генераторах.


    user.js


    var fs = require( 'fs' )
    
    var config
    
    var getConfig = () => {
        if( config ) return config
    
        return config = new Promise( ( resolve , reject ) => {
            fs.readFile( 'config.json' , ( error , configText ) => {
                if( error ) return reject( error )
    
                resolve( JSON.parse( configText ) )
            } )
        } )
    }
    
    module.exports.getName = async () => {
        return ( await getConfig() ).name
    }

    greeter.js


    module.exports.say = async ( greeting , user ) => {
        console.log( greeting + ', ' + ( await user.getName() ) + '!' )
    }

    index.js


    var user = require( './user' )
    var greeter = require( './greeter' )
    
    async function app() {
        console.time('time')
        await greeter.say('Hello', user)
        await greeter.say('Bye', user)
        console.timeEnd('time')
    }
    
    app().catch( error => {
        console.error( error )
    } )

    Волокна


    Несложное нативное расширение для NodeJS реализует полноценные волокна. Всё, что вам нужно — это запустить задачу в волокне и далее, на любом уровне вложенности вызовов функций вы можете приостановить волокно, передав управление другому. В примере далее используются так называемые "фьючеры" (futures), которые позволяют в любой момент синхронизовать одну задачу с другой.


    user.js


    var Future = require( 'fibers/future' )
    var FS = Future.wrap( require( 'fs' ) )
    
    var config
    
    var getConfig = () => {
        if( config ) return config
    
        var configText = FS.readFileFuture( 'config.json' )
        return config = JSON.parse( configText.wait() )
    }
    
    module.exports.getName = () => {
        return getConfig().name
    }

    greeter.js


    А его даже не потребовалось менять — он всё такой же синхронный.


    index.js


    var Future = require( 'fibers/future' )
    
    var user = require( './user' )
    var greeter = require( './greeter' )
    
    Future.task( () => {
    
        try {
    
            console.time('time')
            greeter.say('Hello', user)
            greeter.say('Bye', user)
            console.timeEnd('time')
    
        } catch( error ) {
            console.error( error )
        }
    
    } ).detach()

    При использовании волокон, большая часть кода остаётся синхронной, но в случае необходимости ожидания, блокируется не вся нить, а лишь отдельное волокно. В результате получается как бы параллельное исполнение синхронных волокон.


    Производительность


    Сравним время выполнения основной задачи в каждом варианте многозадачности на NodeJS v6.3.1:


    1. Синхронный код: 4мс.
    2. Предопределённые цепочки: 6мс.
    3. Обещания: 7мс.
    4. Генераторы: 7мс.
    5. Асинхронные функции превращённые в генераторы через Babel: 22мс.
    6. Волокна: 6мс.

    Выводы:


    1. Синхронный код существенно быстрее асинхронного.
    2. Волокна практически не дают пенальти по производительности (только на запуск и переключение волокон).
    3. Обещания и генераторы дают пенальти на вызов каждой функции. В примере у нас мало функций, поэтому просадка не большая.
    4. Babel генерирует весьма паршивый код.

    Отладка


    Давайте посмотрим как наши приложения отреагируют на исключительную ситуацию. Например, в конфиг вместо объекта поместим просто null. Загрузка и парсинг конфига пройдёт нормально, а вот метод getName должен упасть с ошибкой. Мы уже позаботились, чтобы приложение не упало, не проигнорировало ошибку, а залогировало стектрейс в консоль. Вот, что выведут наши реализации:


    Синхронный код


    TypeError: Cannot read property 'name' of null
        at Object.module.exports.getName (./user.js:13:23)
        at Object.module.exports.say (./greeter.js:2:41)
        at Object.<anonymous> (./index.js:7:13)
        at Module._compile (module.js:541:32)
        at Object.Module._extensions..js (module.js:550:10)
        at Module.load (module.js:456:32)
        at tryModuleLoad (module.js:415:12)
        at Function.Module._load (module.js:407:3)
        at Function.Module.runMain (module.js:575:10)
        at startup (node.js:160:18)

    Похоже стектрейс захватил изрядную долю внутренностей NodeJS, но главное, что интересующая нас последовательность вызовов index.js:7 -> say@greeter.js:2 -> getName@user.js:13 присутствует, а значит мы сможем понять как приложение докатилось до этой ошибки.


    Предопределённые цепочки


    TypeError: Cannot read property 'name' of null
        at error (./user.js:31:30)
        at fs.readFile.error (./user.js:20:16)
        at FSReqWrap.readFileAfterClose [as oncomplete] (fs.js:439:3)

    Стектрейс начинается от прихода события о загрузке файла. Что было до этого мы уже не узнаем.


    Обещания


    TypeError: Cannot read property 'name' of null
        at getConfig.then.config (./user.js:19:22)

    Максимально минималистичный стектрейс.


    Генераторы


    TypeError: Cannot read property 'name' of null
        at Object.<anonymous> (./user.js:18:33)
        at next (native)
        at onFulfilled (./node_modules/co/index.js:65:19)

    Тут используются те же обещания со всеми вытекающими отсюда последствиями.


    Асинхронные функции


    TypeError: Cannot read property 'name' of null
        at Object.<anonymous> (user.js:18:12)
        at undefined.next (native)
        at step (C:\proj\async-js\user.js:1:253)
        at C:\proj\async-js\user.js:1:430

    Странно было бы ожидать тут чего-то другого.


    Волокна


    TypeError: Cannot read property 'name' of null
        at Object.module.exports.getName (./user.js:14:23)
        at Object.module.exports.say (./greeter.js:2:41)
        at Future.task.error (./index.js:11:17)
        at ./node_modules/fibers/future.js:467:21

    Всё, что надо и почти ничего лишнего.


    В отладчике вы увидите ту же самую картину: вы сможете пройтись по стеку синхронного и волоконизированного кода, посмотреть значения локальных переменных, поставить точки останова и по шагам пройтись по исполнению вашего приложения. В то же время, код разбитый на цепочки функций, приправленный обещаниями или завёрнутый в генераторы — настоящий кошмар для отладчика. А если вы ещё и кривым транспилятором воспользовались, то храбрости разработчика, взявшегося за отладку этого кода, может позавидовать сам Мустафа Хуссейн.


    Что делать?


    1. Не гнаться за модой, а использовать решения, позволяющие писать лаконичный, быстрый, удобный в отладке код.
    2. Помогать людям в солнцезащитных очках искать путь к свету.
    3. Пропагандировать всесторонний анализ проблематики, вместо проталкивания однобокого мнения.

    Волокна, объективно, по сумме качеств, лучше остальных представленных тут решений. Единственный минус — это ни в коей мере не стандарт и в браузерах даже не планируется к реализации. Но это не столько минус волокон, сколько минус сообщества, которое проталкивает в стандарты обещания, генераторы, асинхронные функции, но совершенно игнорирует куда более простые и прямые решения.


    Ссылки


    1. github:nin-jin/async-js — исходники примеров.
    2. wiki:coroutine — подборка информации о сопрограммах как концепции.
    3. npm:node-fibers — модуль добавляющий волокна в NodeJS.
    Какой подход вы используете сейчас?
    Какой подход вы теперь будете использовать в дальнейшем?

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

    Поделиться публикацией
    Ммм, длинные выходные!
    Самое время просмотреть заказы на Фрилансим.
    Мне повезёт!
    Реклама
    Комментарии 75
    • +4

      Дмитрий, а скорость async/await через babel вы оценивали по транспиляции в ES5 или ES6? Если в ES6, то не пробовали определить, в какой именно части получается такая просадка? (Там помимо генераторов babel вставляет Promise и try-catch.)


      И также интересно, какая будет производительность у нативных async-await из V8, до которых уже немного осталось ждать.

      • 0

        Транспилируется оно в генераторы: https://babeljs.io/docs/plugins/transform-async-to-generator/


        В чём именно просадка затрудняюсь сказать.


        Производительность нативных асинхронных функций должна быть на уровне генераторов.

        • +1

          Даже из стека видно, что используются обещания из core-js, а не нативные, а генераторы компилируются регенератором. Так что здесь вы тестируете совсем не производительность и отладку асинхронные функции и в том, что здесь babel генерирует весьма паршивый код виноваты только вы.

          • 0

            Уже поправил этот косяк. Время работы не изменилось.

            • +1

              Не удивлён, так как здесь складываются издержки приличного числа нативных (далеко не самых быстрых) обещаний и генераторов. А вот от кода babel здесь остатся только оборачивание генератора в хелпер. При желании это хорошо оптимизуется альтернативными преобразованиями и оптимизированными обещаниями из bluebird, как ниже писал ChALkeRx.

              • +2

                Время работы единичного вызова функции на три миллисекунды, большую часть из которых оно читает файл — вообще скорее показатель погоды на Марсе, так что обсуждать его изменения и не изменения довольно бессмысленно.

                • +2

                  Раз вы знаете как надо правильно готовить babel, чтобы он не тормозил, то с нетерпением жду пулреквест.

                  • +1

                    Сомневаюсь в правильности текущего подхода к тестированию, да и, вроде, всё что нужно знать о правилах готовки в данном конкретном случае я уже отписал :)

          • +1

            Дискуссия async/await vs fibers началась несколько дней назад в коментах к моей статье Пишем микросервис на KoaJS 2 в стиле ES2017. Часть I: Такая разная ассинхронность. (название похоже или мне показалось :) ). Там эта цепочка не вызвала бурного обсуждения, но как я вижу, дискуссия не окончена.


            • Стандарт Async Functions будет принят в конце ноября, сейчас он в стадии 'Stage 3 ("Candidate")'. Это означает что Ваш код, написанный под этим синтаксисом меняться не будет и Вас нету рисков что-то переписывать.


            • Неккоректно сравнивать быстродействие async/await в совокупности с транспайлером babel, так как это не самая быстрая реализация этого синтаксиса. Если использовать модуль asyncawait то, по утверждению автора модуля, производительность составлячет 79% от callback-стиля оформления кода. Другими словами потеря в скорости несущественная. Ситуация может измениться в нативной реализации, которая вот-вот, появится.

            • Огромное количество модулей уже описаны промисами, вам стоит просто написать await и ваше приложение ассинхронно подождет.


            • То что у Fibers нету стандарта означает что синтаксис может претерпевать изменения, это существенно повышает риски долгосрочной поддержки такого кода.

            Мне симпатична технология Fibers, но я не понимаю зачем ей противопоставлять async/await. Обе имеют свои плюсы и минусы.

              • +1
                четвертая стадия — как я понимаю, это значит что фича уже утверждена и точно станет частью стандарта ES2017. Круто, ждем скорейшей реализации во всех браузерах.
              • +2
                Дискуссия async/await vs fibers началась несколько дней

                А репозиторий создан в апреле. На самом деле этой дискуссии не один год.


                название похоже или мне показалось :)

                Это пасхалка :-)


                Если использовать модуль asyncawait то, по утверждению автора модуля, производительность составлячет 79% от callback-стиля оформления кода.

                Примечательно, что этот модуль — обёртка над node-fibers.


                Огромное количество модулей уже описаны промисами, вам стоит просто написать await и ваше приложение ассинхронно подождет.

                Лучше я напишу в одном месте Future.fromPromise( p ).wait(), чем буду по всему коду раскидывать async и await.


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

                В том-то и дело, что он не добавляет никакого нового синтаксиса. Только апи для запуска и переключения волокон.


                я не понимаю зачем ей противопоставлять async/await. Обе имеют свои плюсы и минусы.

                Как я обожаю эту толерантность. Обе технологии выполняют одну и ту же функцию. Только одна прямо, хоть и не стандарт, а другая криво, хоть и почти-вот-вот-стандарт.

                • +1
                  Примечательно, что этот модуль — обёртка над node-fibers.
                  Так это еще один плюс в копилку async/await. Смысл для такой "крутой техногии" даунгрейдится в "более примитивную". Кроме того, из обсуждения ниже понятно, что замеры производительности сомнительны даже для babel.

                  Лучше я напишу в одном месте Future.fromPromise( p ).wait(), чем буду по всему коду раскидывать async и await.
                  Сомнительная красота.

                  В том-то и дело, что он не добавляет никакого нового синтаксиса. Только апи для запуска и переключения волокон.
                  А что api меняться не может?

                  Как я обожаю эту толерантность. Обе технологии выполняют одну и ту же функцию. Только одна прямо, хоть и не стандарт, а другая криво, хоть и почти-вот-вот-стандарт.

                  Тут "толерантность" не совсем подходит, синергия в случає с модулем обёрткой над node-fibers.


                  У node-fibers есть сильные стороны, которые вы уже перечислили но есть проблемы:


                  • Есть некий талантливый разработчик Marcel Laverdet, который это все кодит. Ему сейчас интересно, репозиторий живой, но сильная зависимость от одного разработчика это риск для технологии. Ведь не всем она нравиться и ваша голосовалка говорит о том что даже после ваших блестящих аргунетов лидирует async/await


                  • Бинарный код модуля это тоже проблема, т.к. есть облачные хостинги и среды с ограниченными возможностями по компиляции, где Вы не сможете собрать node-fibers.


                  • Изомофный код, который становиться популярным в современных фреймворках не может использовать fibers, т.к. нету для него транспайлера.
                • +1
                  Ситуация может измениться в нативной реализации, которая вот-вот, появится.

                  Уже есть под флагом в браузере (chrome 52). Плюс давно есть в Чакре.

              • 0
                Не хватает квазимногопоточности, а так жирный "+".
                • 0

                  Что такое "квазимногопоточность"?

                  • 0
                    Много потоков на одном ядре
                    • +1

                      Вы точно не про волокна?

                      • 0
                        не подходит, если
                        Много потоков на одном ядре
                        они не смогут прозрачно использовать разделяемые ресурсы и тут
                        нетривиальные механизмы синхронизации
                        . Ваше новопридуманный термин «квазимногопоточность» подходит только для не связанных задач.
                        • 0
                          Это вообще к связям никакого отношения не имеет. Это возможность (как это по русски?) повторного входа в код между его началом и концом. код может относиться как к связанным задачам, так и не связанным. Как пример flash player
                  • +3

                    Я извиняюсь, а вы с какой нагрузкой тестировали? Попробуйте запустить в 50 параллельных запросов от пользователя хотя бы, у вас числа в синхронном случае (и выводы) сразу изменятся.


                    Кроме того, не надо fs.readFile ручками в промис оборачивать, возьмите Bluebird.
                    Кроме того, вы под какой версией Node.js/v8 тестировали? В последнем релизе ещё v8 5.0.x, и там реализация Promise ещё медленная. В 5.3 стало получше (они занялись оптимизацией), но всё ещё не идеально. Пока что есть смысл делать const Promise = require('bluebird') наверху каждого файла.


                    И да, вы код, полученный через Babel точно поверх нативных генераторов гоняли, а не поверх регенератора?


                    Плюс ваш тест слишком маленький, чтобы показать проблемы кода на каллбэках. Посмотрите в сторону https://github.com/petkaantonov/bluebird/tree/master/benchmark, например.

                    • 0
                      Я извиняюсь, а вы с какой нагрузкой тестировали?

                      Ни какой. Просто запускал код из статьи.


                      Попробуйте запустить в 50 параллельных запросов от пользователя хотя бы, у вас числа в синхронном случае (и выводы) сразу изменятся.

                      Не изменятся. Синхронный вариант так и останется быстрее асинхронного. Но при этом многозадачная реализация обслужит в секунду больше клиентов, чем однозадачная, благодаря более эффективному использованию процессорного времени.


                      Кроме того, вы под какой версией Node.js/v8 тестировали?

                      6.3.1


                      Пока что есть смысл делать const Promise = require('bluebird') наверху каждого файла.

                      Получается в полтора раза медленнее.


                      И да, вы код, полученный через Babel точно поверх нативных генераторов гоняли, а не поверх регенератора?

                      Вы правы, у меня использовался пресет es2015, который содержал в том числе и регенератор. Оставил один transform-async-to-generator плагин — по скорости стало даже ещё медленнее.


                      Плюс ваш тест слишком маленький, чтобы показать проблемы кода на каллбэках.

                      Это далеко не самая главная их проблема, чтобы заморачиваться огромными бенчмарками :-)

                      • +1
                        Синхронный вариант так и останется быстрее асинхронного. Но при этом многозадачная реализация обслужит в секунду больше клиентов, чем однозадачная, благодаря более эффективному использованию процессорного времени.

                        Что? Вы, видимо, под «быстрее» что-то другое имеете ввиду =).


                        Получается в полтора раза медленнее.

                        Слабо верится. Как конкретно вы измеряете? Можно увидеть код тестов?


                        Вы правы, у меня использовался пресет es2015, который содержал в том числе и регенератор. Оставил один transform-async-to-generator плагин — по скорости стало даже ещё медленнее.

                        И в то, что transform-async-to-generator в три раза медленнее генераторов, я тоже как-то плохо верю


                        Можете выложить все исходники и то, как вы их запускали/измеряли куда-нибудь?


                        Это далеко не самая главная их проблема, чтобы заморачиваться огромными бенчмарками :-)

                        Основная проблема — читабельность кода и удобство работы с ошибками, там это видно лучше, кмк.

                        • 0
                          Что? Вы, видимо, под «быстрее» что-то другое имеете ввиду =).

                          Время исполнения. А вы по всей видимости имеете ввиду пропускную способность.


                          Слабо верится. Как конкретно вы измеряете? Можно увидеть код тестов?

                          https://github.com/nin-jin/async-js
                          Каждый вариант в отдельной ветке.


                          Можете выложить все исходники и то, как вы их запускали/измеряли куда-нибудь?

                          Статья как бы усеяна ссылками на исходники. Запускаются через npm test.

                          • +2
                            https://github.com/nin-jin/async-js

                            Спасибо.


                            Статья как бы усеяна ссылками на исходники.

                            Не заметил, извините.


                            Я не заметил замер времени у вас, скорее всего потому, что так замерять время ну вот вообще нельзя. Вы измеряете время одного выполнения функции, один раз и на холодную.


                            Да и хорошо бы слить их всё-таки в одну ветку и запускать по очереди.

                            • +4

                              Сравнил.


                              async function test() {
                                await greeter.say('Hello', user)
                                await greeter.say('Bye', user)
                              }
                              
                              async function app() {
                                const start = process.hrtime();
                                for (var i = 0; i < 1000; i++) {
                                  await Promise.all(new Array(100).fill(0).map(test));
                                }
                                console.error(process.hrtime(start));
                              }

                              Bluebird более чем в два раза быстрее получился. stdout пайпил в /dev/null, если медленный вывод убрать и в greeter делать просто await в экспортируемую переменную (чтобы не соптимизировать) — то Bluebird быстрее более чем в три раза.


                              На вашем тесте с одиночным вызовом — да, Bluebird получается чуть медленнее, но это вот вообще ни о чём не говорит. Как и остальные результаты ваших тестов, впрочем.

                              • +1
                                Bluebird более чем в два раза быстрее получился.

                                На всякий случай — в этом сообщении сравнение шло с нативными Promise в Node.js 6.3.1.


                                Сравнение с fibers — ниже, и fibers тоже проигрывает при количестве параллельных запросов больше 5, причём проигрывает очень сильно.

                              • 0

                                Замерьте как считаете правильным :-) Статья вообще не о производительности.

                                • +4

                                  См. выше как правильно.


                                  Статья вообще не о производительности.

                                  Ой, а почему 4 из 4 пунктов выводов касаются производительности и делают какие-то утверждения на основе ваших бенчмарков?


                                  Upd: а, это выводы секции «производительность». Но всё равно при прочтении беглом остаётся такое впечатление, что вы тут говорите, что с async/await и промисами всё медленно и печально, поэтому давайте будем использовать не их, а вот это.

                                  • 0
                                    См. выше как правильно.

                                    Отлично, сравните теперь с волокнами.


                                    Ой, а почему 4 из 4 пунктов выводов касаются производительности?

                                    Потому что они находятся в разделе "Производительность".

                                    • +1
                                      Отлично, сравните теперь с волокнами.

                                      Дайте аналог


                                      async function test() {
                                        await greeter.say('Hello', user)
                                        await greeter.say('Bye', user)
                                      }
                                      
                                      async function app() {
                                        const start = process.hrtime();
                                        for (var i = 0; i < 1000; i++) {
                                          await Promise.all(new Array(100).fill(0).map(test));
                                        }
                                        console.error(process.hrtime(start));
                                      }

                                      на волокнах — сравню. Чтобы вы не говорили, что я всё не так делаю =). На всякий случай — тут мы 1000 раз запускаем по 100 условно параллельных запросов и каждый раз (из 1000) ждём, пока все эти 100 запросов выполнятся.


                                      Потому что они находятся в разделе "Производительность".

                                      Уже заметил и даже успел поправить комментарий до вашего ответа =).

                                      • 0
                                        function test() {
                                            greeter.say('Hello', user)
                                            greeter.say('Bye', user)
                                        }
                                        
                                        function app() {
                                            const start = process.hrtime();
                                            console.time('time')
                                            for (var i = 0; i < 1000; i++) {
                                                Future.wait(new Array(100).fill(0).map(test.future()));
                                            }
                                            console.error(process.hrtime(start));
                                        }
                                        
                                        Future.task( app ).resolve( error => {
                                            if( error ) console.error( error )
                                        } )
                                        • +1

                                          О, не успели. Хотя сейчас проверю и это.

                                          • +1

                                            А так медленнее.


                                            async/await (через babel) — 1.1 секунды, fibers — 1.6 секунд.


                                            Edit: стоп, он расширяет прототип Function?

                                            • 0

                                              А как быстрее? У меня волокна за 1.2 отрабатывают, а то, что babel генерирует — за 2.8.


                                              К сожалению, да.

                                              • +1

                                                Я же написал — вставьте везде const Promise = require('bluebird'). Наверху каждого файла, даже там, где вы руками Promise не вызываете — его вызывает babel.


                                                В greeter.js:


                                                const Promise = require('bluebird');
                                                const fs = Promise.promisifyAll(require('fs'));
                                                
                                                let config;
                                                const getConfig = () => {
                                                  if (config) return config;
                                                  return config = fs.readFileAsync('config.json').then(x => JSON.parse(x));
                                                }

                                                C then — чтобы код был аналогичен вашему в другом месте, но можно и на async переписать:


                                                const Promise = require('bluebird');
                                                const fs = Promise.promisifyAll(require('fs'));
                                                
                                                let config;
                                                async function getConfig() {
                                                  if (config) return config;
                                                  const configText = await fs.readFileAsync('config.json');
                                                  return config = JSON.parse(configText);
                                                }

                                                Так неоптимально, потому что текст не кэшируется и мы реально первые 100 раз читаем файл, но это аналогично тому, что вы c fibers написали. По времени получится 1.3 сек, что всё равно быстрее чем fibers (1.6 сек).


                                                В Babel включаем пресет 'stage-3' и всё. Или плагин 'transform-async-to-generator' и всё.

                                                • +4

                                                  Кстати, попробуйте с числами 1000/100 поиграться.


                                                  У меня такое ощущение, что fibers очень быстрый, когда асинхронность не нужна (то есть когда условное «волокно» одно), но когда их много — он очень сильно сливает.


                                                  На одинаковом неэффективном коде getConfig (см. выше), чтобы не давать никому преимущество, сравнивайте первые две позиции.


                                                  • 100000 раз * 1 параллельный:
                                                    async/await (babel) — 1.5 сек
                                                    fibers — 0.7 сек
                                                    async/await (babel), с правильным кэшем файла — 1.4 сек
                                                  • 10000 раз * 10 параллельных:
                                                    async/await (babel) — 1.3 сек
                                                    fibers — 1.4 сек
                                                    async/await (babel), с правильным кэшем файла — 1.1 сек
                                                  • 1000 раз * 100 параллельных:
                                                    async/await (babel) — 1.3 сек
                                                    fibers — 1.6 сек
                                                    async/await (babel), с правильным кэшем файла — 1.1 сек
                                                  • 100 раз * 1000 параллельных:
                                                    async/await (babel) — 1.7 сек
                                                    fibers — 9.4 сек
                                                    async/await (babel), с правильным кэшем файла — 1.4 сек
                                                  • 25 раз * 4000 параллельных:
                                                    async/await (babel) — 3.4 сек
                                                    fibers — 85.4 сек
                                                    async/await (babel), с правильным кэшем файла — 1.6 сек

                                                  Я правильно понимаю, что если обработку запросов пихать в fibers, то оно очень быстро развалится при увеличении кол-ва одновременных запросов?


                                                  Обратите внимание, что async/await в целом стабилен — ему не важно, какая у вас геометрия.

                                                  • +1

                                                    Да, забыл сказать: после 4000 он начинает ещё сильнее разваливаться, и там явно нелинейная прогрессия — я просто не дождался пока он выполнится.


                                                    Upd: убедился, что дело не в console.log — плющит и без него.

                                                    • –1

                                                      За что я обожаю синтетические тесты, так это за то, что можно получить любые нужные результаты.


                                                      const REQUESTS = 100
                                                      const MEASURES = 1000
                                                      const DEEP = 10
                                                      const CALLS = 10
                                                      
                                                      var Future = require( 'fibers/future' )
                                                      
                                                      function stay() {
                                                          var future = new Future
                                                          setImmediate( ()=> future.return() )
                                                          return future
                                                      }
                                                      
                                                      var test = (() => {
                                                          function inner(j) {
                                                              return ( (j > 0) ? inner(j - 1) : stay().wait() )
                                                          }
                                                      
                                                          for (var i = 0; i < CALLS; ++i) {
                                                              inner(DEEP)
                                                          }
                                                      })
                                                      
                                                      function app() {
                                                          const start = process.hrtime();
                                                          for (var i = 0; i < MEASURES; i++) {
                                                              var tasks = new Array(REQUESTS).fill(0).map(()=>Future.task(test))
                                                              Future.wait( tasks );
                                                              tasks.map( task => task.get() )
                                                          }
                                                          console.log(process.hrtime(start));
                                                      }
                                                      
                                                      Future.task(app).resolve( error => {
                                                          if( error ) console.error( error )
                                                      } )

                                                      7s


                                                      const REQUESTS = 100
                                                      const MEASURES = 1000
                                                      const DEEP = 10
                                                      const CALLS = 10
                                                      
                                                      const Promise = require('bluebird');
                                                      
                                                      function stay() {
                                                          return new Promise( resolve => {
                                                              setImmediate( ()=> resolve() )
                                                          })
                                                      }
                                                      
                                                      async function test() {
                                                          async function inner(j) {
                                                              return await ( (j > 0) ? inner(j - 1) : stay() )
                                                          }
                                                      
                                                          for (var i = 0; i < CALLS; ++i) {
                                                              await inner(DEEP)
                                                          }
                                                      }
                                                      
                                                      async function app() {
                                                          const start = process.hrtime();
                                                          for (var i = 0; i < MEASURES; i++) {
                                                              await Promise.all(new Array(REQUESTS).fill(0).map(test));
                                                          }
                                                          console.log(process.hrtime(start));
                                                      }
                                                      
                                                      app().catch( error => {
                                                          console.error( error )
                                                      } )

                                                      25s

                                                      • +3

                                                        У вас в этом коде ничего асинхронного не происходит вообще — стоит проверять на чём-то более реальном, чем пустой new Promise вокруг setImmediate. Хочу заметить, что в прошлый раз код и пример целиком был ваш, и я только варьировал нагрузку (кол-во параллельных запросов), и хуже всё было при > 5 уже.


                                                        Но даже так, например на 500/100/5/5 — Promise-версия выигрывает в два раза.


                                                        И это даже не самое главное, главное то, что при этом fibers-версия периодически падает на вашем коде с


                                                        ./node_modules/fibers/future.js:471
                                                                                }).run();
                                                                                   ^
                                                        
                                                        RangeError: Maximum call stack size exceeded

                                                        То есть оно вообще не работает.

                                                        • +2

                                                          Поставьте 500/10/1/1 (последние можно как угодно выбирать, по 1 просто для того, чтобы быстрее отработало, да и MEASURES уменьшил просто для ускорения, падает и с вашим тоже) и запустите


                                                          for i in `seq 100`; do node libs/fib.js > /dev/null; done

                                                          У меня где-то в 20% процентов случаев оно тупо падает.

                                                          • 0
                                                            У вас в этом коде ничего асинхронного не происходит вообще

                                                            setImmediate — простейшая асинхронная функция.


                                                            Хочу заметить, что в прошлый раз код и пример целиком был ваш, и я только варьировал нагрузку (кол-во параллельных запросов), и хуже всё было при > 5 уже.

                                                            В прошлый раз вы увеличивали число задач, не меняя числа функций. Пенальти от волокон пропорционально числу задач. А пенальти от генераторов пропорционально числу запусков функций. Всё, что я тут сделал — это увеличил число запусков функций и промисы вновь стали медленнее. Играясь с коэффициентами можно добиться любого результатата.


                                                            На мой взгляд более правдоподобны следующие коэффициенты:


                                                            const MEASURES = 20
                                                            const REQUESTS = 1000
                                                            const DEEP = 10
                                                            const CALLS = 10

                                                            Они дают 9с для генераторов и 7с для волокон.


                                                            И это даже не самое главное, главное то, что при этом fibers-версия периодически падает на вашем коде с

                                                            У меня падает с переполнением стека лишь при DEEP > 16000. Версия на генераторах падает уже при DEEP > 1300.

                                                            • +3
                                                              setImmediate — простейшая асинхронная функция.

                                                              У вас нету никакого асинхронного ввода-вывода и ничего нигде не ждёт. setImmediate сразу же добавляет каллбэк в очередь, ваш код из примера можно переписать на полностью синхронном, добившись такого же порядка выполнения руками. Тогда скорость ещё больше будет, но кому это нужно в реальном мире?


                                                              На мой взгляд более правдоподобны следующие коэффициенты:

                                                              На всякий случай — у вас поменялся порядок констант, так что это 1000/20/10/10 в той форме, как я выше записывал (чтобы никто не запутался).


                                                              И да, у меня на ноуте — 9 с для async/await и 11 сек для fibers на этих параметрах. Но это даже не особо важно, см. ниже.


                                                              У меня падает с переполнением стека лишь при DEEP > 16000. Версия на генераторах падает уже при DEEP > 1300.

                                                              Запустите 100 раз, как я написал выше — оно рандомное. DEEP и CALLS поставьте в 1, дело вообще не в них. Хотя падает и с 10/10, просто работать дольше будет, если не упадёт. REQUESTS поставьте в 500 как у меня или в 1000 как у вас. MEASURES поставьте в 10 или 20 как у вас.

                                                              • +3

                                                                Выкинул всё явно лишнее, вот код (получен из вашего разворачиванием циклов и рекурсии для DEEP=0 и CALLS=1):


                                                                const REQUESTS = 1000
                                                                const MEASURES = 2
                                                                
                                                                const Future = require('fibers/future');
                                                                
                                                                function stay() {
                                                                  const future = new Future;
                                                                  setImmediate(() => future.return());
                                                                  return future;
                                                                }
                                                                
                                                                function test() {
                                                                  stay().wait();
                                                                }
                                                                
                                                                function app() {
                                                                  for (var i = 0; i < MEASURES; i++) {
                                                                    const tasks = new Array(REQUESTS).fill(0).map(() => Future.task(test))
                                                                    Future.wait(tasks);
                                                                    tasks.map(task => task.get());
                                                                  }
                                                                }
                                                                
                                                                Future.task(app).resolve(error => {
                                                                  if (error) console.error(error);
                                                                });

                                                                Запускать так:


                                                                for i in `seq 100`; do node fibfail.js; done 

                                                                У меня упало 24 раза из 100 запусков.

                                                                • 0

                                                                  Есть идеи откуда может браться эта недетерменированность?

                                                                  • +1

                                                                    Честно говоря, мне сейчас немного не до того, чтобы в fibers баги чинить, извините. По недерменированности — банально рейсы в fibers какие-нибудь, например. Я его код не смотрел, не могу точнее сказать =).


                                                                    Да и репортить проблему в fibers я не буду, потому что это ваш код и я даже не вникал в его правильность =).
                                                                    Но могу сообщить информацию об окружении и прочие детали, если у вас проблема не воспроизводится.


                                                                    Если вы думаете, что это проблема самого Node.js — приносите тесткейс без fibers, посмотрим.

                                                                  • +2

                                                                    По поводу примера на основе вашего кода — с REQUESTS = 3000 валится в 90% случаев уже.
                                                                    Упрощённый код:


                                                                    const Future = require('fibers/future');
                                                                    
                                                                    function test() {
                                                                      const future = new Future;
                                                                      setImmediate(() => future.return());
                                                                      future.wait();
                                                                    }
                                                                    
                                                                    function app() {
                                                                      const arr = new Array(3000).fill(0);
                                                                      const a = arr.map(() => Future.task(test));
                                                                      Future.wait(a);
                                                                      const b = arr.map(() => Future.task(test));
                                                                      Future.wait(b);
                                                                    }
                                                                    
                                                                    Future.task(app).resolve(error => {
                                                                      if (error) console.error(error);
                                                                    });
                                                                    • +1

                                                                      Наличие или отсутствие a.map(x => x.get()) и b.map(x => x.get()) ни на что не влияет.

                                                                      • 0

                                                                        Это даёт всплытие исключений. Future.wait не прокидывает исключения, чтобы не маскировать их.

                                                                      • 0

                                                                        В каком окружении вы всё это запускаете? Какая ось? Версия ноды? Архитектура процессора?

                                                                        • +1

                                                                          Linux yoga 4.6.3-1-ARCH #1 SMP PREEMPT Fri Jun 24 21:19:13 CEST 2016 x86_64 GNU/Linux


                                                                          Node.js — 6.3.1 с офсайта (архив) и 6.3.1 из пакетов арча — поведение одинаковое.


                                                                          И да, я пробовал пересобирать fibers — не помогло.

                                                                          • +2

                                                                            Только что проверил на VPS с Debian Jessie и тот же самый архив с офсайта — stack-overflow2.js сегфолтится так же, тест выше не падает, но там рейс может вполне от мощности машины зависеть в том числе. Возможно, если покрутить числа — тоже упадёт, но я сейчас этим заниматься не буду.


                                                                            Успехов в поиске бага, серьёзно =).

                                                                            • +1

                                                                              Если вдруг кто сюда придёт потом — бага зарепорчена в https://github.com/laverdet/node-fibers/issues/299.


                                                                              Цитируя автора библиотеки, кстати:


                                                                              for new projects I think it's the right choice to use those new language features over Fibers.
                                                  • +2

                                                    Так, написал, как получилось — действительно, быстрее вышло. Хотя стоит учесть, что реальном коде ввода-вывода куда больше чем одно чтение из файла на кучу вызовов, которое, к тому же, закэшировано.

                                                    • 0

                                                      В реальном коде обычно глубина стека куда больше, что негативно сказывается на генераторах, но никак не сказывается на волокнах.

                                                      • +3

                                                        См. выше. Эти волокна разваливаются под параллельной нагрузкой.

                                                      • +1

                                                        На всякий случай — это сообщение касалось случая без параллельной нагрузки, то есть если ждать завершения каждого единичного вызова функции test().


                                                        Актуальные результаты выше.

                                    • +3
                                      Разрешите поинтересоваться: обратные звонки, волокна и нити — это особенности авторского стиля, лингвистический патриотизм или гугл-переводчик?
                                      • 0
                                        обратные звонки вместо обратных вызовов еще понятно. А волокна и нити чем не нравятся?
                                        • +1
                                          А multithreading Вы как переводите, извините за нескромный вопрос? Многониточность?
                                          • 0

                                            Было бы классно, но боюсь не поймут.

                                            • 0
                                              Многониточность?

                                              Многонитевость.

                                          • –4

                                            Всего по не многу :-)

                                          • 0
                                            Можете показать в любой js-песочнице как заюзать волокна на примере таймеров?
                                            1) чтобы таймеры выполнялись один за другим
                                            2) выполнялись параллельно
                                          • 0
                                            А что автор может сказать по поводу nodent?
                                            • +1

                                              Оно монкей-патчит require и обработчик расширения *.js?
                                              Зачем так делать, если можно обработать исходники в билдтайме и деплоить/запускать уже обработанные?
                                              Плюс это официально не поддерживается.

                                              • +1

                                                Арр, оно ещё расширяет прототипы функций.

                                            • 0
                                              Пока не пользовался async/await. Но меня мучает вопрос, отладка таких функций происходит тоже условно синхронно? Т.е. например вызвали два раза функцию async и внутри неё поставили брейкпоинт. Отладка будет выполняться для одного вызова внутри функции или как повезет?
                                              • +2
                                                Callback-функции решают не только вопросы многозадачности (где мы являемся инициатором действия), но и прерываний, которые являются гораздо более сложной задачей (чего стоят только приоритеты). В этом вся фишка JavaScript, в котором есть конкретная упрощенная реализация в виде callback-ов. И fibers нам нужны только если стандартная реализация нас не устраивает и мы лезем ниже. Поэтому ничего удивительного в том, что более высокоуровневые упрощенные решения написаны на них. Но сравнивать promise и fibers, например, некорректно.
                                                А написать красивый код без прерываний можно и на callback'ах, благо инструментов достаточно, однако я всячески поддерживаю инициативу в виде async/await.
                                                • 0
                                                  А мне нравится event bus с передачей имени callback object и error:)
                                                • НЛО прилетело и опубликовало эту надпись здесь
                                                  • 0
                                                    Без Event loop асинхр не возможен. По отношению к нему все операции синхронны.

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