Pull to refresh

Comments 78

поменяйте async на non-blocking, пожалуйста.
асинхронность это возможность исполнять несколько действий одновременно, что в рамках однопоточного скрипта невозможно, за исключением воркеров, каждый из которых тоже однопоточный.
Ох, война терминологии :). Кстати, практически везде начинает использоваться бренд «async», тут как с HTML5, наверное придётся смириться :).
просто когда я представляю «умение писать асинхронный скрипт с импользованием jQuery» мне становится дурно. но, похоже, что придется смириться.
UFO landed and left these words here
Некоторые функции «блокируются»: например, чтение килобайта через медленное соединение может подвесить прогу. В таких случаях нужно уметь делать что-то параллельно, не дожидаясь результата. Да и одноядерные процессоры умеют переключать контекст исполнения между thread'ами и процессами.
>В таких случаях нужно уметь делать что-то параллельно, не дожидаясь результата.
concurrency != parallelism
Хорошая статья, давно пора на Хабре больше умного писать и меньше перепечатки новостей :)

Я только не понял идею Do. Что мешает вместо continuableFunc(args)(doSomething, errorHandler) писать continuableFunc(args, doSomething, errorHandler);без всяких умных слов и чуть короче? :)
Я так понял(?), что continuableFunc используются как фабрикатор состояний асинхронной проги, т.е. могут быть использованы по ссылке, а не только в качестве асинхронного вызова.
Continuables можно передавать в Do.chain, Do.map и так далее, для этого они и нужны
Где-то в примерах была уже реализация асинхронности через yield, подобная ADISP.

Не можете подсказать линк?
Насчёт python и неблокирующего нетворкинга смотрим на gevent.org — почти идеальное решение ;-).
Почти. Gevent — это Eventlet, сделанный правильно, но неправильно.
Что правильно:
1. Один способ опрашивать ядро (libevent).
2. Быстрый мейнлуп на C (libevent).
3. Сохранены интерфейсы Event, Queue, Semaphore.
Что неправильно:
1. libevent (надо было libev)
2. Отсутствует возможность патчить socket и прочие модули, чтобы работал нормальный синхронный код. Это есть в eventlet.

Что неправильно в Eventlet:
1. Много способов опрашивать события (хабы: select, epoll, libevent, libev, правда последний давно не поддерживается и выкинут из сорцов).
2. Мейнлуп на питоне (частично из-за пункта 1).
3. Отклонения от стандартных интерфейсов Event, Queue, Semaphore. Особенно Event.
> Что неправильно:
> 1. libevent (надо было libev)

На самом деле все бенчмарки libev относительно libevent очень стары, никто не делал сравнений с новыми версиями. Libevent более распространён (memcached, chromium, ...) и более стабилен, к тому же там есть httpd, httpc, асинхронный револьвер DNS. Я бы не сказал libev более уместен тут.

> 2. Отсутствует возможность патчить socket и прочие модули, чтобы работал
> нормальный синхронный код. Это есть в eventlet.

Такая возможность есть — модуль gevent.monkey.
Если честно, то цифры в бенчмарке несколько странные.
Бенчмарк действительно очень полный, в отличие от приведенного мною. Признаться, не хотел свести все к сравнению питонячих веб-серверов.

Вспомнил, что разработчик fapws в 3-й версии отказался от libevent в пользу libev (должно быть не зря). Наверное корректнее было посмотреть бенчмарк fapws2 vs fapws3, хотя на результаты скорее всего сильно повлияет другая база кода.
Майкры работают над Async Workflows, вот так это выглядит:

let file = File.OpenRead(«Program.fs»)
let! stuff = file.AsyncRead(250) // Тут считывается файл, неблокируя и асинхронно

printfn "%A" (Encoding.Default.GetString(stuff)) // А тут синхронно печатается в консольку

let req = WebRequest.Create(«www.google.com/search?q=node.js»)
let! resp = req.AsyncGetResponse() // Тут скачивается НТМЛ, тож неблокируя и асинхронно
printfn "%i headers recieved." resp.Headers.Count // Опять синхронно печатается в консольку

Это F#. Все асинхронно, и все неблочаще. Доступно уже года два или больше.

Я у себя в блоге (http://chaliy.name/blog/2010/7/node_dot_net) всегото пару дней тому назад возмущался, что node.js аж нисколечки нечетабельный.
Не совсем честно сравнивать спец. ЯП :), у автора в статье тоже есть примеры, да и новые ЯП со встроенной асинх. давно есть.
Это F#, язык общего назначения, с функциональным уклоном. Эти воркфлоу реализованы стандартными средствами языка. Средство называется Computation Expressions.
Ну точно так же можно вспомнить Erlang (хотя там в принципе другая идеология) или Go. Смысл статьи показать, как асинхронность можно использовать в широко используемых ЯП.
Ну что скажешь, молодцы. Хотя могли бы и элегантнее :-)
Да мне тож так кажется. Например восклицательный знак из let прибрать, слово Async. Но фишка в том что это заимплеменчено стандарными сердствами языка. Тоесть теоретически такую же штуку можно провернуть, чтобы запустить этот код на разных машинах. Или например что-бы вставить туда MSMQ. Или сделать его выполняем в браузере (компилится в javascript — WebSharper). Или… масса применений.
В плане асинхронности — да.
Но там будут свои геморрои — например, меньше полезных либ, некоторые кривые, поэтому какие-то очевидные вещи, которые на js/python/etc… делаются сходу, здесь потребуют времени.
надеюсь в fprog появиться эта статья, а пока можно почитать только это:
node.js vs. Erlang — groups.google.com/group/erlang-russian/browse_thread/thread/cf3c65b0c5230834

>Но там будут свои геморрои
так везде есть свои проблемы, область применимости и требования к качеству конечного продукта

ps я в эрланге ни бум бум, кроме написания hello world :) но язык концептуально меня поразил больше чем node.js
Erlang — простой, надежный, но ограниченный язык
JS куда более универсален

PS Я в Erlang'e бум-бум :-)
какие ограничения есть у этого языка?
Огромное количество очень суровых ограничений, именно в них его сила.
Отсутствие деструктивного присваивания, немутируемые в принципе данные, общение между потоками только посылкой сообщений, на каждом шагу возникает необходимость создания компонентов в виде мини-серверов, ужасающая рудиментарная поддержка объектности… Список можно продолжить.

Эти ограничения заставляют решать задачи определенным способом, который как правило и является правильным, если только вы не пытаетесь сделать что-то, для чего язык совершенно не предназначен. Но это ограничивает область его применения.

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

Про ноду говорить не совсем верно, правильнее про JS. А он зарекомендовал себя как вполне универсальный — чего только на нем не пишется и все вполне удобно и успешно. И серверные решения, и GUI, и злая математика. Основная проблема была в скорости, но V8 хорошо подсобил.
В руби 1.9 можно писать синхронный код используя non-blocking вызововы, достигается это благодаря связке EventMachine + Fibers. Подробней об этой технике можно почитать тут. Есть даже рельсы, которые используют non-blocking io — github.com/igrigorik/async-rails
Вспомнилась жуткая JS-библиотечка Concurrent.Thread
jsthread.sourceforge.net/
Статья авторов на английском в pdf тут.
Треды там условные, но она как раз позволяет писать асинхронный (неблокирующий) код в синхронном стиле. Как это работает? Ваши функции вычитываются в строку через Function.prototype.toString, перепарсиваются, циклы разматываются в goto-style, и в некоторых местах вставляется вызов, передающий управление таск-свитчеру. Затем это снова превращается в функции (то ли через new Function, то ли через eval). В статье есть пример с Ajax-запросом. Страшные тормоза, но красивый код =) Если узкое место — это связь с сервером по Ajax, то эту радость теоретически даже можно использовать. Я сам не пробовал :-)
> Если узкое место — это связь с сервером по Ajax

можно использовать web workers
На мой взгляд, система колбэков — не самая большая проблема неблокирующих вызовов.
Главная проблема — работа с состоянием программы (переменные). Если представить себе программу в виде графа с вершинами-состояниями (она им и является), то обычная синхронная программа описывается более или менее читаемо. На вершинах обычно пишут изменения вектора состояния, предполагая остальные переменные — инвариантами относительно соседних состояний.

А вот когда мы связываемся с неблокирующими вызовами колбэков, это, фактически, может означать что ЛЮБОЙ обработчик может быть вызыван при ЛЮБОМ векторе состояний. Мы, фактически, теряем вообще все инварианты в программе!

Задумайтесь!

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

Хотите наглядный пример?
В яваскрипте загружаю набор данных со стороннего сервера. JQuery::json. И всё бы хорошо, но выборку данных формирует пользователь через интерфейс. К тому времени, когда данные придут, пользователь может нащёлкать интерфейс, много чего поменяв в выборке. В обработчике приходится полностью проверять состояние интерфейса и срочно придумывать что из полученной выборки пригодится, если требования пользователя изменились.

И хорошо если интерфейс описывается 2-3 переменными. Когда настроек много, то есть длинный вектор состояний запросов, проверка всего длинного вектора и принятие решение о дополнительной обработке выборки — много дополнительного анализа и работы по его реализации.
Мне кажется вы думаете не в ту сторону. Не надо требовать от технологии то, для чего она не предназначена — в данном случае для уточнения выборки. Либо оптимизируйте серверную часть, чтобы все выглядело красиво, либо просто делайте новую выборку не показывая старую, если состояние интерфейса изменилось.
Я привёл довольно «щадящий» пример. Проблема на самом деле гораздо серьёзнее.
Когда пишете любую функцию, подумайте, сколько переменных вы считаете инвариантами.
А теперь представьте, что инвариантов больше нет.
И вам надо (в общем случае) проверять все переменные.
Например, к моменту вызова обработчика может идти шатдаун интерфейса; или идёт фильтрация предыдущих данных. Да всё что угодно может быть.
Да нет той проблемы, о которой вы пишете.

При правильной архитектуре каждый конкретный кусок кода имеет дело лишь с частью состояния, на остальное ему плевать. И эта часть не должна быть слишком большой.
Это и дает возможность существовать языкам, в которых данные как правило вообще немутабельны (взять тот же Erlang, к примеру).

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

Что касается вашего примера, простейшее решение — перезапрос. Желание использовать результаты прошлой выборки для текущего состояния — это попытка жесткой оптимизации, как правило не оправданная.
Вы же сами пишете: «при правильной архитектуре». Синтаксис языка никак эту структуру не обуславливает. Значит, проблема всё-таки есть.
Сформулируем ещё раз: проблема создания правильной архитектуры несинхронных программ: неблокирующих (попроще) и асинхронных (посложнее).

Я был бы признателен, если вы напишете продолжение вашей статьи, где бы подробно описали эту архитектуру и детали её проектирования.

К примеру, для асинхронных программ, мне очень нравится подход Эрланга, который разделяет переменные потоков. Очень нравится система обмена сообщениями через очередь.

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

Но всё не даётся даром. Через какое-то время я ощутил, что подобный подход требует более серьёзной работы с инвариантами. Их, в отличие от внутренних переменных потока, нельзя изолировать от обработчика, потому что это вектор состояния потока-обработчика, а не вектор потока-инициатора сообщения. Я прихожу ко мнению, что асинхронная очередь сообщений (колбэков), вполне возможно, предполагает стэковую организацию векторов состояний потоков. В общем, это большая тема. И если у вас есть знания и серьёзные наработки в этой области, я бы с огромным интересом почитал вашу статью.
Проблемы нет, потому что есть подходы, наработки и традиции, которые позволяют строить архитектуру правильно. Что такое «правильно» — сильно зависит от проекта, поэтому тут лучше говорить предметно.

Вы говорите, вам понравился подход Erlang'а — замечательно, используйте его. Erlang обеспечивает полную изоляцию переменных, запрещая даже простейшее деструктивное присваивание — изолировано все от всего, не важно внутреннее оно или принадлежит обработчику. Для нормальной скорости работы это требует суровых оптимизаций, copy-on-write — наверное самая примитивная из них.

На C++ такое реализовать будет непросто. Но запрещать принципиальную возможность «сделать не так» не обязательно, часто достаточно соглашений. Или используйте языки со встроенным нужным подходом — Erlang далеко не единственный предоставляет изоляцию, почти любой язык из семейства функциональных поступает подобным образом. Почитайте доклад Валкина, может под ваши задачи хорошо ляжет тот же OCaml. На C++ свет клином не сошелся.
Я каждый раз когда смотрю на node.js вижу одну большую монаду на ручной тяге :-)
а зачем вы нарочно нечестно устрашили первый пример? ведь в синхронном вызове ничего не говорилось об обработке ошибок. некрасивый психологический трюк.
я лично со временем так привык к асинхронности, что практически её не замечаю, сидит в мозгу, как родная

var f = syncOpen(args);
checkConditions(f);
var result = syncReadAll(f);
checkResult(result);

asyncOpen(args, function(f){
  checkConditions(f);
  asyncReadAll(f, function(result){
    checkResult(result);
  });
});
— Синхронный вызов ожидаемо кинет исключение, а в асинхронном я должен сделать это руками, так что никакого «нарочно нечестно»
— Даже если забить на обработку ошибок, это не отменяет описанной проблемы

А в простых случаях — да, даже существующий синтаксис вполне приемлим.
если обработку ошибки проводить, то проводить её и там и там. ато в асихронном примере на 50% больше логики. естественно, он будет казаться тяжелее.
Кстати, по-моему у Ruby и Coffeescript самая красивая и короткая лямбда, что важно при асинхронном коде.
Ruby:
asyncOpen('file') { |file|
  file.read { |content|
    checkResult(content)
  }
}

Coffeescript:
asyncOpen ''file', (file) =>
  file.read (content) =>
    checkResult(content)
Для меня в JS синтаксис более читаемый, скорее это дело привычки
Вот слово function — действительно длинновато :-)
В следующей версии ECMAscript (Harmony) function уберут. Склоняются вроде к #{ }.
Главное, чтобы лишнего в язык не понапихали, а то так и стремятся, а язык от этого сильно проиграет
В Actionscript3 практически всё основано на асинхронных событиях — ничего с читабельностью не случается — код понятен и читаем. А вот анонимные коллбэки — зло.
И не надо говорить что AS3- однопоточен — это не имеет значения.
В C# тоже очень удобно сделаны события (сигнал-слот)
Асинхронность помогает читабельности. А вот анонимные функции в javascript надо вообще отменить отдельным указом — с расстрелом за невыполнение.
Была же классная спека ES4 — почему остались на убогом ES3.5?
чем вам не нравятся анонимные функции в Javascript? так, чисто любопытно
Позвольте я встряну… Тем, что во многом они делают один и тот же функционал, но тело их каждый раз целиком указано в каждом вызове. Анонимность вообще во многом зло — когда пишешь сам, она терпима, но когда код коммитишь (показываешь другим) — зло, зло, зло. Потому что вместо того, чтобы вчитываться в логику работы программы, вчитываешься в скобочки. И получается за деревьями не видно леса.
Особенно в такую жару =) Буквально вчера рефакторил полдня, чтобы и без того убитый мозг мог хоть как-то читать.
а чем плоха анонимная ф-ция, если она не повторяется и не собирается повторятся?
Если она действительно не повторится — ничем.
Но такое бывает крайне редко, к сожалению. В основном есть куча перенагруженного кода.
я сейчас разрабатываю приложение. там куча таких анонимных ф-ций, которые никогда не повторятся. но такие вещи на жс режко разрабатываются, да
Навешивание событий (как в jQuery) предполагает большое кол-во анонимных функций, которые не повторяются.
Практически в любом языке, к. поддерживает анонимные объявления и события, события можно объявлять так. Вот только почти в каждой книге а-ля «совершенный код» или там «лучшие практики» такого подхода советуют не придерживаться.

Но это уже совсем не касается предмета разговора и ведет на скользскую дорожку холивара. =)
Странные вы книги читали ;). В Java и C# нверное не рекомендуется. В Ruby анонимными функциями даже по массиву ходят вместо foreach. В тех же рекомендациях по jQuery тоже самое — везде анонимки. В языках, где широко применяется функции высшего порядка из ФП (map, each, filter) точно так же рекомендуют использовать именно эти функции, то есть передавать логику обработки массива, как анонимную функцию.
Почему же сразу страшные? Совершенно верно: как раз по Java. =)
У меня оттуда и сохранилась привычка — сохранять как можно меньше анонимности. Конечно, создавать отдельный класс ради 3 строк — глупость, но если там хотя бы одно ветвление…

Я слишком радикально написал. Ниже (сейчас прямо под моим комментарием), dimsmol написал лучше некуда: «Анонимные колбэки — очень большое добро при правильном использовании».
Ох, странные у меня мутировали в страшные, но не суть.
Ну так у Java совсем другая идеология и подход. Не надо его распространять на Javascript и другие динамические ЯП. Ну как бы нет абсолютных истин. Есть несколько набор вещей, которые друг с другом уживаются. У динамических ЯП один набор истин, у Java/C# другая.
Меня например очень смущают, когда в JS тянут идеологию Java — получаются монстры типа ExtJS. JS ближе к Ruby/Python, чем к Java.
Не надо меня уговаривать. =)
В любом ЯП идеология одна: писать лаконичный и понятный код. Анонимность как раз на острие этого постулата.
Когда я смотрел на JS со стороны опыта на Java, то JS мне не нравился. Но когда я увидел jQuery, то почувствовал прекрасную и стройную философию JS, которую теперь люблю (хотя у языка есть много слабых мест, типа длинного синтаксиса и отсутствия method_missing).
А ещё меня смущает, что из-за образования в универе Pascal→C→Java программисты не знают о существовании других философий языка, типа ФП с LISP и т. д.
Анонимные колбэки — очень большое добро при правильном использовании. У меня куча кода под рукой, который без них выглядел бы куда более страшно и убого.
Сигнал-слот — хорошая абстракция, но тяжеловесная, не всегда удобная.

Всему свое место.
Кажется вам нехватает знания понятий со-процедуры (co-procedures) и продолжения (continuations), через которые они реализуются.
>> со-процедуры (co-procedures)

вы про сопрограммы (coroutines)?

>> и продолжения (continuations), через которые они реализуются.

сопрограммы необязательно реализовывают через continuations.

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

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

Заранее извиняюсь за своё не слишком профессиональное в этих вопросах мнение, но по-моему это говорит о том, что нужен новый язык, в котором всё было бы асинхронно само по себе и всем этим расписыванием занимался бы компилятор/интерпретатор.
Статья как раз о том, что и в старых языках все может быть шоколадно :-)
> фактически прервет исполнение в точке вызова и когда-нибудь начнет его снова из этой же точки, используя сохраненный контекст

Я может чего-то не понимаю, но это просто описание синхронного программирования фактически :))
Поскольку продолжать исполнение будет ровно тот, кого мы запустили асинхронно и никто другой (нам нужны именно те данные результата, которые только он возвращает.

Другими словами нужно просто весь этот синхронный кусок из 4х синхронных вызовов выделить в отдельный асинхронный поток средствами языка или библиотек. Разве нет?

Или вы предполагаете что этот сохраненный контекст кто-то другой сможет использовать в другом месте? Пока представляется только режим отладчика, который может возобновлять выполнение в разрез с задуманной заранее последовательностью действий.
Проблема в том, что исполняющий движок что JS, что питона, что многих других языков, в текущей реализации будет ждать возврата из библиотечной функции, заблокировав исполнение в том числе других нитей — в каждый момент времени только одна нить может что-то делать и она «как-бы делает». Неблокирующие вызовы должны позволить продолжать исполнение других нитей во время ожидания ввода-вывода и прочего подобного, когда на самом ничего могущего помешать другим нитям не происходит.
Only those users with full accounts are able to leave comments. Log in, please.