Pull to refresh

Асинхронное программирование на JavaScript — Остаться в живых

Reading time 14 min
Views 35K
Original author: Werner Schuster и Dionysios G. Synodinos
Программисты принимают некоторые особенности как должное — последовательное программирование, к примеру, при записи алгоритма, который делает один шаг только после другого.

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

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

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

Сообщество JavaScript в курсе этого, особенно сообщество Node.JS, так как Node.JS ставит акцент на асинхронном коде.

Группа CommonJS ответила на этот вызов в форме Promises (Обещаний), которые нацелены на обеспечение интерфейса для взаимодействия с объектом, который представляет собой результат действия, которое выполняется асинхронно, и может или не может быть закончено в любой данный момент времени.
Таким образом, различные компоненты могут вернуть обещания для асинхронных действий и потребители могут использовать обещания предсказуемым образом. Обещания можем также обеспечить фундаментальную сущность, чтобы быть использованными для более удобного синтаксически уровня расширений языка, которые помогают работать с асинхронностью.

Stratified JavaScript — другой подход, предлагающий упростить программирование с помощью надмножества языка JavaScript. Но если вы не можете менять языки программирования, можно использовать гибкий API, который позволяет эмулировать последовательный код. Если этот API позволяет использовать краткие обозначения, его часто называют встроенным DSL.

InfoQ решил взглянуть на список этих API и DSL, и пообщаться с их создателями о том, как они подошли к проблеме, о принципах проектирования, о парадигмах, которым они следуют, и о многом другом. И, конечно, — об ограничениях этих решений.

В частности, InfoQ связался c:


InfoQ: На решении каких проблем сосредоточена ваша библиотека? То есть, она сосредоточена главным образом на удалении шаблонного кода, уходе от ручной обработки асинхронного ввода/вывода, или же она также обеспечивает дирижирование или другой функционал (например, чтобы помочь с одновременной обработкой нескольких I/O-вызовов с ожиданием результатов их выполнения, и так далее).

Tim (Step): Цели Step состоят как в удалении шаблонного кода, так и в большей читаемости асинхронного кода. Наша библиотека весьма минималистична, и не делает ничего, что вы не можете повторить вручную с обильным использованием блоков try..catch и переменных-счётчиков. Особенностью нашей библиотеки является простое формирование цепочек последовательных вызовов с опциональными группами параллельных вызовов на каждом шаге.

Will (Flow-js): Flow-JS предоставляет конструкцию JavaScript, которая похожа на continuation (преемственность) или fiber (волокно), существующие в других языках програмирования. Практически, она может быть использована для уничтожения так называемых «пирамид» из вашей многошаговой асинхронной логики. Вместо того, чтобы непосредственно использовать литералы вложенных функций обратного вызова, вы используете специальное значение «this» как функцию обратного вызова для следующей функции, указанной в определении потока исполнения.

Kris Zyp (node-promise): Проблема в том, что типичный стиль потока выполнения обратных вызовов (стиль передачи преемственности) объединяет усложнение интерфейса и смешивание функциональных параметров с обработчиками результатов. Promises (обещания) инкапсулируют событийное завершение вычисления, позволяя функциям/методам исполняться с чистыми входными параметрами, в то время, как возвращаемое значение, как обещание, хранит результат.

Я объяснил эти принципы чуть подробнее здесь и здесь.

Инкапсулируя событийное вычисление, обещания прекрасно работают для дирижирования параллельными и последовательными действиями, даже со сложной условной логикой. Библиотека node-promise включает функции, чтобы выполнять это просто (функции all() и step() в promised-io)

Кстати, просто для информации, promised-io фактически наследник node-promise. Она имеет тоже самое ядро, но promised-io также включает в себя функции ввода/вывода NodeJS в стиле обещаний, и в ней доступен слой нормализации платформы, который обеспечивает доступ к аналогичным функциям в браузере.

Caolan (Async): Да, основная задача состоит в удалении шаблонного кода. Код для вызова функций последовательно или параллельно, с последующим ожиданием обратных вызовов в JavaScript, довольно подробен, но очевиден. Вскоре после того, как node.js перешёл от обещаний к обратным вызовам в качестве базового механизма обработки асинхронности, я обнаружил, что я использую одни и те же шаблоны снова и снова. Казалось очевидным, что их нужно экстрагировать их в отдельную библиотеку.

С тех пор она выросла, чтобы охватить более сложные возможности, которые позволяют дирижировать обратными вызовами на основе их зависимостей. По большей части, однако, это довольно низкоуровневая библиотека, которая оставляет общую структуру разработчикам. Тем не менее, я счёл, что JavaScript часто подходит для более функционального стиля программирования, и тут же добавил асинхронные версии map, reduce, filter и других обычных функций. Библиотека действительно проявляет свою силу, когда используется в таком стиле, и позволяет придерживаться обычных обратных вызовов без использования продолжений или объектов-обещаний.

Fabian (Async.js): Async.js пытается упростить стандартные асинхронные шаблоны в JavaScript. Её главное назначение — применить серию асинхронных функций к множеству однородных объектов. Она выросла из асинхронной функции forEach в набор обобщённых концепций. Это особенно удобно при асинхронной работе с файловой системой в node.js, хотя библиотека не привязана к node.js и может быть использована для любых других похожих случаев. Этот кусочек кода показывает применение async.js:
async.readdir(__dirname)
  .stat()
  .filter(function(file) {
    return file.stat.isFile()
  })
  .readFile("utf8")
  .each(function(file) {
    console.log(file.data);
  })
  .end(function(err) {
    if (err)
      console.log("ERROR: ", err);
    else
      console.log("DONE");
  });
Этот код оперирует всеми элементами текущего каталога. Это однородный набор объектов. Для каждого элемента в каталоге выполняется последовательность асинхронных операций. Сперва отфильтровываются все элементы, не являющиеся файлами, затем содержимое файлов читается с диска в кодировке utf-8 и печатается на консоль. После выполнения всех операций вызывается финальная функция обратного вызова с параметром-индикатором ошибки.

AJ (FuturesJS): Довольно сложно рассуждать об асинхронном и событийно-управляемом программировании.

Я создал Futures в основном для:

  • предоставления одной библиотеки асинхронного потока управления как для браузера, так и для сервера (Node.JS);
  • публикации качественного шаблона обработки обратных вызовов и вызовов обработчиков ошибок;
  • контроля потока исполнения приложения, в котором события зависят друг от друга;
  • обработки обратных вызовов для множества ресурсов, таких, как мэш-апы;
  • поощрения использования передовых практик программирования, таких, как использование моделей и обработка ошибок.


Futures.future и Futures.sequence просто сокращают количество шаблонного кода и предоставляют некоторую гибкость.

Futures.join может подсоединять (в той же манере, как join работает для потоков исполнения операционной системы) или синхронизировать (для событий, которые случаются периодически) несколько объектов future.

Futures.chainify делает простым создание асинхронных моделей, похоже на Twitter Anywhere API.

Isaac (slide-flow-control): Задача, решению которой посвящён slide, состоит в том, что мне было нужно что-то, о чём я бы мог рассказать на встрече OakJS, и не хотел придумывать какую-либо новую идею, так как я очень ленивый. В основном, я хотел не делать практически никакой работы, просто показать сделанное, выпить немного пива, полакомиться китайской кухней, пообщаться с интересными людьми, немного искупаться в положительном внимании, и затем вернуться домой. Соотношение объёма работы к полученному выигрышу очень важно для меня, как в программном обеспечении, так и в жизни. Таким образом, я просто скомпоновал вместе простые до ужаса вспомогательные асинхронные методы, которые я использую в npm, так, что они уместились в набор слайдов, назвал это «slide» из-за этой особенности, и представил эту библиотеку.

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


InfoQ: Реализует ли библиотека идеи учёных компьютерной науки?
Tim (Step): Напрямую — ничего.

Will (Flow-js): Не то, чтобы я знаю. Это был просто мой первый удар на создание бизнес-логики, которая упрощает выполнение множества синхронных вызовов внешних сервисов, управляемых из Node.js.

Kris Zyp (node-promise): Да, конечно. Большинство компьютерных научных исследований по асинхронному дизайну указывает на Promises (обещания) в различных формах, как наиболее подходящий механизм для функциональных потоков и правильного разделения интересов. Термин «promise» был первоначально предложен Daniel P. Friedman и David Wise, начиная с 1976 года. Вы можете узнать больше о богатой истории компьютерной науки относительно обещаний из статьи на Википедии.

Caolan (Async): У меня нет опыта в сфере компьютерной науки, и я реализовал библиотеку Async на чисто прагматической основе. Когда мне требовалась функция высшего порядка, чтобы вычистить немного асинхронности в JavaScript, и я использовал её неоднократно, она попадала в библиотеку.

Fabian (Async.js): Реализация async.js отдаленно напоминает Монады Haskell, но это скорее случайно.


AJ (FuturesJS): Да. Наибольшее влияние оказали следующие материалы:

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

Isaac (slide-flow-control): Она использует один из видов шаблона continuation. Множество исследований компьютерной науки упускают эту тему, я думаю. Это я тычу пальцем в Луну. Чтобы попасть туда, вам нужна ракета. Более длинные пальцы не помогут. Когда Вы осознаете это, более глубокие тайны проявят себя.


InfoQ: Предлагает ли библиотека какие-либо стратегии обработки ошибок? Как она взаимодействует с выбрасыванием исключений?

Tim (Step): Если исключение выбрасывается на любом шаге, оно ловится и передается следующему шагу как параметр-ошибка. Также любые не-неопределённые возвращаемые значения передаются на следующий шаг как параметр обратного вызова. Таким образом, шаги могут быть синхронными или асинхронными с использованием одного и того же синтаксиса.

Will (Flow-js): Flow-JS не имеет никакой встроенной обработки исключений, что определённо является её слабой стороной. Tim Caswell написал модуль, основанный на flow, под названием «Step», который обрамляет вызовы каждой из предоставленных функций в блоки try/catch и передаёт пойманные исключения следующей функции в последовательности.

Kris Zyp (node-promise): Да, promises спроектированы, чтобы предоставить асинхронный эквивалент синхронного потока выполнения. Как функция JavaScript может выбросить исключение или успешно вернуть значение, так и promise может быть разрешено к успешному значению или к состоянию ошибки. Обещания, возвращаемые вызывающим методам, могут распространять ошибки до тех пор, пока обработчик ошибок не «поймает» их, точно так же, как выброшенное исключение распространяется до тех пор, пока не будет поймано. Библиотека node-promise кроректно поддерживает эту концепцию, позволяя легко регистрировать обработчики ошибок или распространять ошибки до тех пор, пока они не будут пойманы (чтобы исключить ситуацию молчаливого проглатывания ошибок). Имея прямые синхронные эквиваленты обещаний, поток исполнения кода с использованием обещаний очень легко читать.

Caolan (Async): Обработка исключений с асинхронным кодом может быть немного сложнее, особенно если вы не знакомы с сильно асинхронными средами, такими, как node.js. Для меня, на самом деле, обработка ошибки важнее, чем стиль, который вы адаптируете для этой обработки. В браузере это особенно важно, поскольку в нём легко случайно поднять исключение до самого верхнего уровня, тем самым убив весь JavaScript на странице.

Есть многое, что можно сказать по этому поводу. Обработка исключений должна быть прямой и желательно привычной, так чтобы было легко её реализовывать и становилось очевидно, если Вы забыли об этом. К сожалению, JavaScript в браузере не помогает нам в этом пока в достаточной мере, но node.js предоставил простую конвенцию, которая может быть легко использована в обеих средах.

Библиотека Async приняла именно эту конвенцию, используя первый аргумент обратного вызова для передачи ошибки к следующему шагу в вашей программе. Если первый аргумент является null (или иным ложным значением), то его можно не учитывать, в противном случае оно рассматривается как исключение. Где это возможно, выполнение будет произведено библиотекой Async по сокращённой схеме, чтобы ускорить процесс. Если одна функция из выполняемой коллекции выполняется с ошибкой, то последующие функции в коллекции не будут выполнены.

Fabian (Async.js): Async.js построена с использованием конвенции обработки ошибок node.js. Первый аргумент любой функции обратного вызова зарезервирован за объектом ошибки. Если вычисление заканчивается ошибкой, или возникает исключение, ошибка/исключение передаются как первый аргумент в функцию обратного вызова. Async.js поддерживает две стратегии обработки ошибок, что может быть настроено через её API. В случае ошибки либо останавливается целиком операция над множеством, и вызывается обработчик ошибок, либо ошибочный элемент пропускается.

AJ (FuturesJS): Так как исключения не могут быть «выброшены» асинхронно, вместо этого пользователю предлагается передавать любые исключения как первый параметр в функцию обратного вызова.

Основная идея, — выполнить try {} catch(e) {} для ошибки, и передать ошибку, вместо того, чтобы остановить приложение в какой-то неопределённый момент времени. Futures.asyncify() делает это для того, чтобы использовать синхронные функции в доминирующем асинхронном окружении.

Здесь пример:

(function () {
 "use strict";

 var Futures = require('futures'),
  doStuffSync,
  doStuff;

 doStuffSync = function () {
  if (2 % Math.floor(Math.random()*11)) {
   throw new Error("Some Error");
  }

  return "Some Data";
 };

 doStuff = Futures.asyncify(doStuffSync);

 doStuff.whenever(function (err, data) {
  if (err) {
   console.log(err);
   return;
  }
  console.log(data);
 });

 doStuff();
 doStuff();
 doStuff();
 doStuff();
}());


Isaac (slide-flow-control): Никогда не бросайте исключения! Никогда! Выбрасывание исключения является злом. Не делайте этого. Когда вызывается функция обратного вызова, первым аргументом будет либо ошибка, либо null. Если это ошибка, обработайте её, или передайте функции обратного вызова для обработки. Передавайте ошибку первым параметром вашей функции обратного вызова, чтобы сигнализировать о появлении ошибки.


InfoQ: Был ли вдохновитель или под каким влиянием была создана ваша библиотека (например, F# Workflows, Rx (версия для Javascript), или другие проекты)?

Tim (Step): Да, стиль был заимствован напрямую из проекта flow-js.

Will (Flow-js): Не совсем. Это было просто первое решение, которое пришло мне в голову.

Kris Zyp (node-promise): На библиотеку node-promise повлияли язык программирования E от Mark Miller и использование обещаний в нём, библиотека ref_send от Tyler Close, библиотека Q от Kris Kowal, библиотека NarrativeJS от Neil Mix, реализации Deferred в каркасах Twisted и Dojo, и многие другие библиотеки.

Caolan (Async): Я боюсь, что так как я не использовал F# или Rx, то я не могу выразить своё отношение к этим проектам. Однако, я черпал вдохновение из Underscore.js, отличной функциональной библиотеки для программирования на JavaScript. Большинство функций, которые используют итераторы, из Underscore были изменены, чтобы начать работать асинхронно с обратными вызовами, и внедрены в библиотеку Async.

Fabian (Async.js): Характерные цепочки вызовов в API были инспирированы jQuery. Одной из моих целей было предоставить jQuery-подобный API для модуля файловой системы node.js. Очень большое влияние также оказали генераторы в стиле python. Каждый элемент в цепочке генерирует значения, которые могут быть использованы последующими элементами цепочки. Вся операция вызывается последним элементов в цепочке, который «протягивает» значения через цепочку. С этой точки зрения async.js отличается от jQuery и Rx, где значения проталкиваются источником. Эта «тянущая» система делает возможным вычислять все значения лениво, а также позволяет создавать генераторы, которые возвращают бесконечное множество значений (к примеру, все чётные целые числа).

AJ (FuturesJS): Не сразу, нет.

Я построил мэшап-сайт с использованием Facebook и Amazon, и моей первой попыткой стала мешанина, поскольку я просто не понимал, как работать с моделью, созданной из двух ресурсов (Я не был на самом деле хорошо знаком с JavaScript в то время; Я шёл путём проб и ошибок в стиле «WTFJS» и использовал немного jQuery, чтобы упростить болезненную работу с DOM).

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

Я пробовал, терпел неудачи, и наполовину справился с использованием нескольких различных методов, и затем, к моей удаче, кто-то в рассылке моей локальной группы пользователей JavaScript упомянул о серии лекций Кроуфорда о JS. После просмотра всей серии (я смотрел третий раздел по крайней мере 3 раза) я, наконец, стал лучше понимать, как управлять «проблемами» (или, скорее, возможностями) асинхронного программирования. Далее я нашёл слайды Кроуфорда и начал с примера promises, который он привёл, как со своей стартовой точки.

Позднее я начал играться с Node.JS, и вследствие этого изменил свою стратегию обработки ошибок (но документацию обновил только несколько дней назад). В Futures 2.0, которую я выпущу в ближайшее воскресенье, я также добавил EventEmitter из Node.JS для использования в браузере.

Isaac (slide-flow-control): Нет. Это было, я полагаю, вдохновлено шаблонами, к которым мы пришли в NodeJS для использования обратных вызовов. Я просто сторонник полноты, потому что я недостаточно умён, чтобы помнить более чем одного вида вещей (или, может быть двух, в хороший день, с большим количеством кофе) без того, чтобы запутываться и ходить в туалет, думая, что это ванная комната, а затем получив всю одежду пахнущей, как… Впрочем, Вы поняли идею.

Использовать вещи одного типа, везде. Вот и все. Функции, которые принимают обратный вызов в качестве последнего аргумента. Функции обратного вызова, получающие сообщение об ошибке в качестве первого аргумента, или null/undefined, если все прошло успешно. Slide — это всего несколько вспомогательных функций, которые делают легче выполнение кучи вещей, используя эту схему.


InfoQ: Существуют ли какие-либо новые возможности или изменения в языке JavaScript, которые могут сделать библиотеки лучше, например, позволят быть более кратким, и т.д.?

Tim (Step): Возможно, но не без серьезных изменений в семантике языка. Может быть, препроцессор типа coffeescript может помочь с синтаксисом, но я думаю, что лучше придерживаться ванильного JavaScript большую часть времени.

Will (Flow-js): На мой взгляд, Javascript отчаянно нуждается в что-то вроде волокон из Ruby 1.9. Я провел много времени, работая с Node.js для моих будущих проектов, но в какой-то момент асинхронное программирование всегда заставляет мозг плавиться. Есть много инструментов для того, чтобы сделать его более управляемым, но я не могу доказать, но чувствую, что наличие столь большого числа библиотек, как Flow-js, является только свидетельством того, что Javascript на самом деле неудачен для параллельного программирования.

Я знаю, что одной из целей Node было избежание модификаций в ядре JavaScript-движка V8, но, насколько я знаю, ребята из Asana добавили волокна в ядро без особых проблем.

Kris Zyp (node-promise): Да, недавно была дискуссия вокруг одно-кадровых или мелких продолжений, похожих на генераторы, которые могут помочь избежать необходимости обратного вызова, которые усложняют ветвление и циклы потоков. Это может быть использовано в сочетании с обещаниями для создания чрезвычайно простого и удобного для чтения асинхронного кода.

Кстати, еще одно замечание, — библиотека node-promise также реализует спецификацию http://wiki.commonjs.org/wiki/Promises/A, то есть она может взаимодействовать с Dojo и, вероятно, с будущими JQuery promises.

Caolan (Async): библиотека Async была разработана, чтобы максимально использовать язык в его нынешнем виде. Для работы как есть, без попыток создать новый язык поверх JavaScript.

Тем не менее, добавление yield в JavaScript 1.7 может иметь некоторые интересные приложения для будущих проектов. С использованием yield было бы возможно портировать некоторые Twisted-подобные функции на JavaScript для более синхронно-подобного стиля кодирования. Это то, что мой коллега изучает с Whorl, хотя этот проект, похоже, перестал развиваться.


Fabian (Async.js): Стандартизация генераторов и итераторов, поддерживаемых Mozilla, может сделать код async.js более кратким и упростить проблемы асинхронности.

AJ (FuturesJS): Наиболее часто повторяющийся код в библиотеке, это тот, который делает одинаковой работу кода в браузере и в Node.JS. Я знаю, некоторые библиотеки (такие, как teleport) были созданы, чтобы попытаться решить этот вопрос, но я не игрался пока ни с одной из них. Это, конечно, было бы неплохо, если бы асинхронный require был встроен в язык.

С моей точки зрения, язык с такой естественной асинхронностью, как JavaScript, должен иметь что-то похожее на фьючерсы, встроенным в ядро языка.

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

Isaac (slide-flow-control): Нет. Моя библиотека управления потоком лучшая. Она не может быть улучшена каким-либо иным способом, потому что эта лучшесть непосредственно связана с моей самостью, поэтому любое внешнее влияние сделало бы ее менее моей, и таким образом, менее лучшей. Если вы хотите получить опыт, я предлагаю вам написать свою библиотеку. Вы увидите, что она лучшая, сразу, как только вы её напишете. Если какой-либо другой библиотеке кажется, что что-то могло бы быть лучше, то вы можете торопиться обратно в редактор, скрывая свой стыд, и быстро переизобретать все свои идеи, но уже немного иначе, чем раньше, и тогда вы будете знать, в вашем сердце, что ваше новое сейчас лучшее.
Tags:
Hubs:
+30
Comments 22
Comments Comments 22

Articles