Здравствуйте, меня зовут Дмитрий Карловский и я… многозадачный человек. В смысле у меня много задач и мало времени, чтобы их все уже, наконец, закончить. Отчасти это и к лучшему — всегда есть чем заняться. С другой стороны — пока ты разрываешься между проектами, мир катится куда-то не туда и некому забраться на броневик и призвать толпу остановиться и немного подумать. А вопрос-то серьёзный — долгое время мир JS был погружён в ад обратных звонков и с ними не только не боролись — их боготворили. Потом он чуть менее чем полностью погряз в обещаниях. Сейчас к ним с разных сторон усиленно вставляют подпорки разной степени кривизны. А света в конце тоннеля всё не видать. Но обо всём по порядку...
Теория многозадачности
Сперва определимся с терминами. В процессе работы, приложение выполняет различные задачи. Например, "скачать файл с удалённого сервера" или "обработать запрос пользователя".
Не редки ситуации, когда для выполнения одной задачи требуется выполнение дополнительных задач — "подзадач". Например, для обработки запроса пользователя, необходимо скачать файл с удалённого сервера.
Запустить подзадачу мы можем синхронно, и тогда текущая задача заблокируется в ожидании завершения подзадачи. А можем запустить асинхронно, и тогда текущая задача продолжит своё выполнение не дожидаясь завершения подзадачи.
Тем не менее, обычно для завершения выполнения задачи, пусть и не сразу, но требуется и завершение выполнения подзадачи с последующей обработкой её результатов. Блокировку одной задачи в ожидании сигналов от другой будем называть "синхронизацией". В общем случае, синхронизация одних и тех же задач может происходить и множество раз, по самой различной логике, но в дальнейшем мы будем рассматривать лишь простейший и самый распространённый вариант — синхронизацию по завершению подзадачи.
В языках, поддерживающих многопоточность, обычно каждая задача запускается в отдельном "системном потоке" или (более правильно) "нити". Каждая нить может исполняться на отдельном ядре процессора, параллельно с другими нитями. Так как нитей может быть много, а число ядер весьма ограничено, то операционная система реализует механизм "вытесняющей многопоточности", когда любая нить, если она долго исполняется, может быть принудительно приостановлена, чтобы дать возможность поработать другим нитям.
Параллельная работа задач приводит к различным проблемам при работе с общей памятью, для решения которых приходится использовать нетривиальные механизмы синхронизации. Чтобы упростить работу программиста и повысить надёжность, производимого им программного обеспечения, некоторые языки полностью отказываются от многопоточности и запускают все задачи в одной единственной нити. Многозадачность в этом случае реализуется одним из следующих способов:
Волокна (fibers), также известные как "сопрограммы" (coroutines). По сути это те же нити, но реализующие "кооперативную многозадачность". Все волокна имеют свои стеки, но исполняются в рамках одной нити, а значит не могут исполняться параллельно. При этом решение о том, когда переключить нить на другое волокно, принимает само волокно.
Цепочки задач. Суть подхода в том, что вместо того, чтобы приостанавливать текущую задачу на время выполнения подзадачи, мы разбиваем задачу на много маленьких подзадач и говорим каждой, какую подзадачу нужно выполнить по завершении этой.
Конечные автоматы (state machine), также известные как "генераторы" (generators), "асинхронные функции" (async functions) и "полусопрограммы" (semicoroutines) и "сопрограммы без стека" (stackless coroutines). Фактически, это объекты, хранящие локальное состояние единственного метода, в начале которого находится ветвление с переходом к коду одного из шагов исходной задачи. По завершении шага управление возвращается вызвавшей функции. Повторный вызов асинхронной функции уже приводит к переходу к другому шагу.
Реализации на NodeJS
В репозитории nin-jin/async-js в отдельных ветках собраны реализации простого приложения на разных моделях многозадачности. Суть приложения простая и состоит из 3 частей:
- Модель (user.js). Загружает конфиг с диска и предоставляет метод для получения имени пользователя из этого конфига.
- Отображение (greeter.js). Принимает модель пользователя и печатает, обращение к нему в консоль.
- Контроллер (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:
- Синхронный код: 4мс.
- Предопределённые цепочки: 6мс.
- Обещания: 7мс.
- Генераторы: 7мс.
- Асинхронные функции превращённые в генераторы через Babel: 22мс.
- Волокна: 6мс.
Выводы:
- Синхронный код существенно быстрее асинхронного.
- Волокна практически не дают пенальти по производительности (только на запуск и переключение волокон).
- Обещания и генераторы дают пенальти на вызов каждой функции. В примере у нас мало функций, поэтому просадка не большая.
- 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
Всё, что надо и почти ничего лишнего.
В отладчике вы увидите ту же самую картину: вы сможете пройтись по стеку синхронного и волоконизированного кода, посмотреть значения локальных переменных, поставить точки останова и по шагам пройтись по исполнению вашего приложения. В то же время, код разбитый на цепочки функций, приправленный обещаниями или завёрнутый в генераторы — настоящий кошмар для отладчика. А если вы ещё и кривым транспилятором воспользовались, то храбрости разработчика, взявшегося за отладку этого кода, может позавидовать сам Мустафа Хуссейн.
Что делать?
- Не гнаться за модой, а использовать решения, позволяющие писать лаконичный, быстрый, удобный в отладке код.
- Помогать людям в солнцезащитных очках искать путь к свету.
- Пропагандировать всесторонний анализ проблематики, вместо проталкивания однобокого мнения.
Волокна, объективно, по сумме качеств, лучше остальных представленных тут решений. Единственный минус — это ни в коей мере не стандарт и в браузерах даже не планируется к реализации. Но это не столько минус волокон, сколько минус сообщества, которое проталкивает в стандарты обещания, генераторы, асинхронные функции, но совершенно игнорирует куда более простые и прямые решения.
Ссылки
- github:nin-jin/async-js — исходники примеров.
- wiki:coroutine — подборка информации о сопрограммах как концепции.
- npm:node-fibers — модуль добавляющий волокна в NodeJS.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (73)
babylon
07.08.2016 21:49Не хватает квазимногопоточности, а так жирный "+".
vintage
08.08.2016 00:04Что такое "квазимногопоточность"?
babylon
08.08.2016 00:26Много потоков на одном ядре
novoxudonoser
08.08.2016 17:06не подходит, если
Много потоков на одном ядре
они не смогут прозрачно использовать разделяемые ресурсы и тутнетривиальные механизмы синхронизации
. Ваше новопридуманный термин «квазимногопоточность» подходит только для не связанных задач.babylon
10.08.2016 00:55Это вообще к связям никакого отношения не имеет. Это возможность (как это по русски?) повторного входа в код между его началом и концом. код может относиться как к связанным задачам, так и не связанным. Как пример flash player
ChALkeRx
07.08.2016 22:01+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, например.
vintage
08.08.2016 00:39Я извиняюсь, а вы с какой нагрузкой тестировали?
Ни какой. Просто запускал код из статьи.
Попробуйте запустить в 50 параллельных запросов от пользователя хотя бы, у вас числа в синхронном случае (и выводы) сразу изменятся.
Не изменятся. Синхронный вариант так и останется быстрее асинхронного. Но при этом многозадачная реализация обслужит в секунду больше клиентов, чем однозадачная, благодаря более эффективному использованию процессорного времени.
Кроме того, вы под какой версией Node.js/v8 тестировали?
6.3.1
Пока что есть смысл делать const Promise = require('bluebird') наверху каждого файла.
Получается в полтора раза медленнее.
И да, вы код, полученный через Babel точно поверх нативных генераторов гоняли, а не поверх регенератора?
Вы правы, у меня использовался пресет
es2015
, который содержал в том числе и регенератор. Оставил одинtransform-async-to-generator
плагин — по скорости стало даже ещё медленнее.
Плюс ваш тест слишком маленький, чтобы показать проблемы кода на каллбэках.
Это далеко не самая главная их проблема, чтобы заморачиваться огромными бенчмарками :-)
ChALkeRx
08.08.2016 00:49+1Синхронный вариант так и останется быстрее асинхронного. Но при этом многозадачная реализация обслужит в секунду больше клиентов, чем однозадачная, благодаря более эффективному использованию процессорного времени.
Что? Вы, видимо, под «быстрее» что-то другое имеете ввиду =).
Получается в полтора раза медленнее.
Слабо верится. Как конкретно вы измеряете? Можно увидеть код тестов?
Вы правы, у меня использовался пресет es2015, который содержал в том числе и регенератор. Оставил один transform-async-to-generator плагин — по скорости стало даже ещё медленнее.
И в то, что transform-async-to-generator в три раза медленнее генераторов, я тоже как-то плохо верю
Можете выложить все исходники и то, как вы их запускали/измеряли куда-нибудь?
Это далеко не самая главная их проблема, чтобы заморачиваться огромными бенчмарками :-)
Основная проблема — читабельность кода и удобство работы с ошибками, там это видно лучше, кмк.
vintage
08.08.2016 00:56Что? Вы, видимо, под «быстрее» что-то другое имеете ввиду =).
Время исполнения. А вы по всей видимости имеете ввиду пропускную способность.
Слабо верится. Как конкретно вы измеряете? Можно увидеть код тестов?
https://github.com/nin-jin/async-js
Каждый вариант в отдельной ветке.
Можете выложить все исходники и то, как вы их запускали/измеряли куда-нибудь?
Статья как бы усеяна ссылками на исходники. Запускаются через
npm test
.ChALkeRx
08.08.2016 01:09+2https://github.com/nin-jin/async-js
Спасибо.
Статья как бы усеяна ссылками на исходники.
Не заметил, извините.
Я не заметил замер времени у вас, скорее всего потому, что так замерять время ну вот вообще нельзя. Вы измеряете время одного выполнения функции, один раз и на холодную.
Да и хорошо бы слить их всё-таки в одну ветку и запускать по очереди.
ChALkeRx
08.08.2016 01:24+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 получается чуть медленнее, но это вот вообще ни о чём не говорит. Как и остальные результаты ваших тестов, впрочем.
vintage
08.08.2016 01:27Замерьте как считаете правильным :-) Статья вообще не о производительности.
ChALkeRx
08.08.2016 01:44+4См. выше как правильно.
Статья вообще не о производительности.
Ой, а почему 4 из 4 пунктов выводов касаются производительности и делают какие-то утверждения на основе ваших бенчмарков?
Upd: а, это выводы секции «производительность». Но всё равно при прочтении беглом остаётся такое впечатление, что вы тут говорите, что с async/await и промисами всё медленно и печально, поэтому давайте будем использовать не их, а вот это.
vintage
08.08.2016 01:51См. выше как правильно.
Отлично, сравните теперь с волокнами.
Ой, а почему 4 из 4 пунктов выводов касаются производительности?
Потому что они находятся в разделе "Производительность".
ChALkeRx
08.08.2016 02:04+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 запросов выполнятся.
Потому что они находятся в разделе "Производительность".
Уже заметил и даже успел поправить комментарий до вашего ответа =).
vintage
08.08.2016 02:28function 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 ) } )
ChALkeRx
08.08.2016 02:33+1А так медленнее.
async/await (через babel) — 1.1 секунды, fibers — 1.6 секунд.
Edit: стоп, он расширяет прототип Function?
vintage
08.08.2016 02:36А как быстрее? У меня волокна за 1.2 отрабатывают, а то, что babel генерирует — за 2.8.
К сожалению, да.
ChALkeRx
08.08.2016 02:50+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' и всё.
ChALkeRx
08.08.2016 03:20+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 в целом стабилен — ему не важно, какая у вас геометрия.
ChALkeRx
08.08.2016 03:26+1Да, забыл сказать: после 4000 он начинает ещё сильнее разваливаться, и там явно нелинейная прогрессия — я просто не дождался пока он выполнится.
Upd: убедился, что дело не в
console.log
— плющит и без него.
vintage
08.08.2016 10:24-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
ChALkeRx
08.08.2016 10:55+3У вас в этом коде ничего асинхронного не происходит вообще — стоит проверять на чём-то более реальном, чем пустой
new Promise
вокруг setImmediate. Хочу заметить, что в прошлый раз код и пример целиком был ваш, и я только варьировал нагрузку (кол-во параллельных запросов), и хуже всё было при > 5 уже.
Но даже так, например на 500/100/5/5 — Promise-версия выигрывает в два раза.
И это даже не самое главное, главное то, что при этом fibers-версия периодически падает на вашем коде с
./node_modules/fibers/future.js:471 }).run(); ^ RangeError: Maximum call stack size exceeded
То есть оно вообще не работает.
ChALkeRx
08.08.2016 11:10+2Поставьте 500/10/1/1 (последние можно как угодно выбирать, по 1 просто для того, чтобы быстрее отработало, да и MEASURES уменьшил просто для ускорения, падает и с вашим тоже) и запустите
for i in `seq 100`; do node libs/fib.js > /dev/null; done
У меня где-то в 20% процентов случаев оно тупо падает.
vintage
08.08.2016 11:27У вас в этом коде ничего асинхронного не происходит вообще
setImmediate — простейшая асинхронная функция.
Хочу заметить, что в прошлый раз код и пример целиком был ваш, и я только варьировал нагрузку (кол-во параллельных запросов), и хуже всё было при > 5 уже.
В прошлый раз вы увеличивали число задач, не меняя числа функций. Пенальти от волокон пропорционально числу задач. А пенальти от генераторов пропорционально числу запусков функций. Всё, что я тут сделал — это увеличил число запусков функций и промисы вновь стали медленнее. Играясь с коэффициентами можно добиться любого результатата.
На мой взгляд более правдоподобны следующие коэффициенты:
const MEASURES = 20 const REQUESTS = 1000 const DEEP = 10 const CALLS = 10
Они дают 9с для генераторов и 7с для волокон.
И это даже не самое главное, главное то, что при этом fibers-версия периодически падает на вашем коде с
У меня падает с переполнением стека лишь при DEEP > 16000. Версия на генераторах падает уже при DEEP > 1300.
ChALkeRx
08.08.2016 11:42+3setImmediate — простейшая асинхронная функция.
У вас нету никакого асинхронного ввода-вывода и ничего нигде не ждёт.
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 как у вас.ChALkeRx
08.08.2016 11:54+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 запусков.
vintage
08.08.2016 15:12Есть идеи откуда может браться эта недетерменированность?
ChALkeRx
08.08.2016 15:23+1Честно говоря, мне сейчас немного не до того, чтобы в fibers баги чинить, извините. По недерменированности — банально рейсы в fibers какие-нибудь, например. Я его код не смотрел, не могу точнее сказать =).
Да и репортить проблему в fibers я не буду, потому что это ваш код и я даже не вникал в его правильность =).
Но могу сообщить информацию об окружении и прочие детали, если у вас проблема не воспроизводится.
Если вы думаете, что это проблема самого Node.js — приносите тесткейс без fibers, посмотрим.
ChALkeRx
08.08.2016 23:37+2Знаете, решил на его тесты посмотреть.
https://github.com/laverdet/node-fibers/blob/master/test/stack-overflow2.js вообще стабильно (100%) сегфолтится.
Может, ну его?
ChALkeRx
09.08.2016 08:30+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); });
vintage
09.08.2016 10:33В каком окружении вы всё это запускаете? Какая ось? Версия ноды? Архитектура процессора?
ChALkeRx
09.08.2016 12:39+1Linux 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 — не помогло.
ChALkeRx
09.08.2016 12:48+2Только что проверил на VPS с Debian Jessie и тот же самый архив с офсайта —
stack-overflow2.js
сегфолтится так же, тест выше не падает, но там рейс может вполне от мощности машины зависеть в том числе. Возможно, если покрутить числа — тоже упадёт, но я сейчас этим заниматься не буду.
Успехов в поиске бага, серьёзно =).
- 100000 раз * 1 параллельный:
ChALkeRx
08.08.2016 02:29+2Так, написал, как получилось — действительно, быстрее вышло. Хотя стоит учесть, что реальном коде ввода-вывода куда больше чем одно чтение из файла на кучу вызовов, которое, к тому же, закэшировано.
RuddyRudeman
07.08.2016 22:51+3Разрешите поинтересоваться: обратные звонки, волокна и нити — это особенности авторского стиля, лингвистический патриотизм или гугл-переводчик?
sugadu
07.08.2016 23:46Можете показать в любой js-песочнице как заюзать волокна на примере таймеров?
1) чтобы таймеры выполнялись один за другим
2) выполнялись параллельноChALkeRx
07.08.2016 23:54+1Никак, это нативный модуль, и в браузере он работать не будет.
Хотя если считать https://tonicdev.com/npm/fibers js-песочницой, то можно =).
Оно на стороне сервера выполняется.
Veber
08.08.2016 01:02Пока не пользовался async/await. Но меня мучает вопрос, отладка таких функций происходит тоже условно синхронно? Т.е. например вызвали два раза функцию async и внутри неё поставили брейкпоинт. Отладка будет выполняться для одного вызова внутри функции или как повезет?
arvitaly
08.08.2016 04:56+2Callback-функции решают не только вопросы многозадачности (где мы являемся инициатором действия), но и прерываний, которые являются гораздо более сложной задачей (чего стоят только приоритеты). В этом вся фишка JavaScript, в котором есть конкретная упрощенная реализация в виде callback-ов. И fibers нам нужны только если стандартная реализация нас не устраивает и мы лезем ниже. Поэтому ничего удивительного в том, что более высокоуровневые упрощенные решения написаны на них. Но сравнивать promise и fibers, например, некорректно.
А написать красивый код без прерываний можно и на callback'ах, благо инструментов достаточно, однако я всячески поддерживаю инициативу в виде async/await.
staticlab
Дмитрий, а скорость async/await через babel вы оценивали по транспиляции в ES5 или ES6? Если в ES6, то не пробовали определить, в какой именно части получается такая просадка? (Там помимо генераторов babel вставляет Promise и try-catch.)
И также интересно, какая будет производительность у нативных async-await из V8, до которых уже немного осталось ждать.
vintage
Транспилируется оно в генераторы: https://babeljs.io/docs/plugins/transform-async-to-generator/
В чём именно просадка затрудняюсь сказать.
Производительность нативных асинхронных функций должна быть на уровне генераторов.
rock
Даже из стека видно, что используются обещания из
core-js
, а не нативные, а генераторы компилируются регенератором. Так что здесь вы тестируете совсем не производительность и отладку асинхронные функции и в том, что здесь babel генерирует весьма паршивый код виноваты только вы.vintage
Уже поправил этот косяк. Время работы не изменилось.
rock
Не удивлён, так как здесь складываются издержки приличного числа нативных (далеко не самых быстрых) обещаний и генераторов. А вот от кода
babel
здесь остатся только оборачивание генератора в хелпер. При желании это хорошо оптимизуется альтернативными преобразованиями и оптимизированными обещаниями изbluebird
, как ниже писал ChALkeRx.ChALkeRx
Время работы единичного вызова функции на три миллисекунды, большую часть из которых оно читает файл — вообще скорее показатель погоды на Марсе, так что обсуждать его изменения и не изменения довольно бессмысленно.
vintage
Раз вы знаете как надо правильно готовить babel, чтобы он не тормозил, то с нетерпением жду пулреквест.
rock
Сомневаюсь в правильности текущего подхода к тестированию, да и, вроде, всё что нужно знать о правилах готовки в данном конкретном случае я уже отписал :)
apelsyn
Дискуссия async/await vs fibers началась несколько дней назад в коментах к моей статье Пишем микросервис на KoaJS 2 в стиле ES2017. Часть I: Такая разная ассинхронность. (название похоже или мне показалось :) ). Там эта цепочка не вызвала бурного обсуждения, но как я вижу, дискуссия не окончена.
Стандарт Async Functions будет принят в конце ноября, сейчас он в стадии 'Stage 3 ("Candidate")'. Это означает что Ваш код, написанный под этим синтаксисом меняться не будет и Вас нету рисков что-то переписывать.
Огромное количество модулей уже описаны промисами, вам стоит просто написать await и ваше приложение ассинхронно подождет.
Мне симпатична технология Fibers, но я не понимаю зачем ей противопоставлять async/await. Обе имеют свои плюсы и минусы.
rock
Сейчас стандарт на четвёртой стадии.
Laney1
четвертая стадия — как я понимаю, это значит что фича уже утверждена и точно станет частью стандарта ES2017. Круто, ждем скорейшей реализации во всех браузерах.
vintage
А репозиторий создан в апреле. На самом деле этой дискуссии не один год.
Это пасхалка :-)
Примечательно, что этот модуль — обёртка над node-fibers.
Лучше я напишу в одном месте Future.fromPromise( p ).wait(), чем буду по всему коду раскидывать async и await.
В том-то и дело, что он не добавляет никакого нового синтаксиса. Только апи для запуска и переключения волокон.
Как я обожаю эту толерантность. Обе технологии выполняют одну и ту же функцию. Только одна прямо, хоть и не стандарт, а другая криво, хоть и почти-вот-вот-стандарт.
apelsyn
Тут "толерантность" не совсем подходит, синергия в случає с модулем обёрткой над node-fibers.
У node-fibers есть сильные стороны, которые вы уже перечислили но есть проблемы:
Есть некий талантливый разработчик Marcel Laverdet, который это все кодит. Ему сейчас интересно, репозиторий живой, но сильная зависимость от одного разработчика это риск для технологии. Ведь не всем она нравиться и ваша голосовалка говорит о том что даже после ваших блестящих аргунетов лидирует async/await
Бинарный код модуля это тоже проблема, т.к. есть облачные хостинги и среды с ограниченными возможностями по компиляции, где Вы не сможете собрать node-fibers.
faiwer
Уже есть под флагом в браузере (chrome 52). Плюс давно есть в Чакре.