Асинхронность: почему это никак не сделают правильно?

    Асинхронные программы чертовски неудобно писать. Настолько неудобно, что даже в node.js, заявленном как «у нас все правильное-асинхронное», понадобавляли таки синхронных аналогов асинхронных функций. Что уж говорить про питоновский синтаксис, не дающий объявить лямбду со сколь-либо сложным кодом внутри…

    Забавно, что красивое решение проблемы не требует ничего экстраординарного, но почему-то до сих пор не реализовано.

    Суть проблемы


    Допустим, у нас есть такой синхронный код:
    var f = open(args);
    checkConditions(f);
    var result = readAll(f);
    checkResult(result);

    Асинхронный аналог будет выглядеть куда страшнее:
    asyncOpen(args, function(error, f){
      if(error)
        throw error;
      checkConditions(f);
      asyncReadAll(f, function(error, result){
        if(error)
          throw error;
        checkResult(result);
      });
    });

    Чем длиннее цепочка вызовов, тем страшнее код.

    Может, вам недостаточно страшно? Тогда попробуйте написать аналог следующего кода, заменив все вызовы на асинхронные:
    while(true)
    {
      var result = getChunk(args1);
      while(needsPreprocessing(result))
      {
        result = preprocess(result);
        if(!result)
          result = obtainFallback(args2);
      }
      processResult(result);
    }

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

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

    Решения с помощью кирки и лома


    Возможно, вы видели в node.js такую концепцию, как Promise. Так вот, ее больше нет. Самые обычные колбэки оказались куда человечнее. Поэтому про Promise я рассказывать не буду.

    А расскажу я про библиотеку Do. Эта библиотека базируется на концепции continuables. Вот пример, демонстрирующий разницу подходов:
    // callback-style
    asyncFunc(args, function(error, result){
      if(error)
        throw error;
      doSomething(result);
    });

    // continuables-style
    var continuable = continuableFunc(args);
    continuable(function(result){ // callback
      doSomething(result);
    }, function(error){ // errback
      throw error;
    });

    // continuables-style short
    continuableFunc(args)(doSomething, errorHandler);

    Continuable — это функция, которая возвращает другую функцию, которая принимает в качестве параметров callback и errback и совершает асинхронный вызов.

    В некоторых случаях такой подход позволяет заметно упростить код — взгляните на «continuables-style short». Здесь в качестве колбэка мы используем непосредственно doSomething, так как сигнатура функции нам подходит, а в качестве errback используем некий «стандартный» errorHandler, определенный где-то еще.

    Do умеет многое. Параллельные вызовы, асинхронный map, некоторые другие интересности. Подробнее об этом можно почитать в статье "Комбо-библиотека Do". Там же можно прочесть о том, как конвертировать функции, заточенные под callback-style (стандартный для node.js) в continuables-style.

    Однако, вернемся к примерам, с которых я начал. Чем может Do помочь в нашем случае? Собственно, вот чем:
    Do.chain(
      continuableOpen(args),
      function(f){
        checkConditions(f);
        return continuableReadAll(f);
      }
    )(function(result){
      checkResult(result);
    }, errorHandler);

    Это continuables-style аналог самого первого примера. Ну что ж, может чуточку лучше по сравнению с callback-style, а может и нет. По крайней мере рост отступов с ростом длины цепочки остановлен, а обработчик ошибок сконцентрировался в одной точке. Но код выглядит страшно, особенно в сравнении с исходной синхронной версией в четыре строки. Более сложный пример — тот что с циклами — Do вообще не по зубам, снова придется городить страшенный огород.

    yield спешит на помощь


    Кирка и лом не помогли, хочется чего-то возвышенного. Хочется, чтобы асинхронный вызов был не сложнее синхронного. А в идеале — почти от него не отличался. И это возможно.

    Лучше всего инфраструктура решения описана у Ивана Сагалаева в статье "ADISP". ADISP — это написанная им питоновская библиотека, которая и приносит счастье.

    Нечто похожее можно собрать и на JS, примером служит Er.js, но туда понапихали многовато магии для первого знакомства, поэтому рекомендую именно статью Сагалаева.

    Подход, примененный в ADISP, позволяет писать код в следующем стиле:
    var func = process(function(){
      while(true)
      {
        var result = yield getChunk(args1);
        while(yield needsPreprocessing(result))
        {
          result = yield preprocess(result);
          if(!result)
            result = yield obtainFallback(args2);
        }
        yield processResult(result);
      }
    });

    Да, это тот самый страшный пример с циклами. Все вызовы асинхронные. Обрамляющая функция func приведена только для того, чтобы показать, что ее придется задекорировать. process — декоратор, аналогичный описанному у Сагалаева. getChunk, needsPreprocessing, preprocess, obtainFallback, processResult — асинхронные функции, задекорированные декоратором async в терминологии ADISP.

    Подход работает везде, где есть yield в питоновском стиле. То есть, превосходный асинхронный node.js в пролете, поскольку V8 еще не поддерживает yield.

    Нативное решение


    Нужно ли что-то еще, когда используя трюк с yield мы можем добиться столь достойных результатов? Считаю, что да, поскольку:

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

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

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

    Неблокирующие библиотечные функции

    Как правило, асинхронные функции реализованы либо в ядре языка (например, setTimeout), либо в библиотечных функциях (например, функции модуля fs в node.js). Таким образом, проблема красивых асинхронных вызовов в первую очередь имеет отношение именно к библиотечным функциям.

    Это означает замечательную вещь — красивые асинхронные вызовы могут быть введены простым добавлением специального соглашения для асинхронных библиотечных функций. Не нужно менять язык, не нужно придумывать новое ключевое слово и ломать голову над обратной совместимостью. Просто дайте автору библиотеки способ указать, что асинхронной библиотечной функции нужен способ вернуть к жизни контекст, из которого ее вызвали, а текущее исполнение нужно прекратить. Такие функции можно будет смело использовать, например, так:
    while(true)
    {
      doSomePeriodicTask();
      nbSleep(1000);
    }

    Здесь nbSleep — неблокирующий вызов sleep, который фактически прервет исполнение в точке вызова и когда-нибудь начнет его снова из этой же точки, используя сохраненный контекст в качестве колбэка.

    Пусть нам даже придется иметь пары функций — одну обычную, с колбэком (все-таки в некоторых случаях вариант с колбэком предпочтительнее), а вторую неблокирующую. Это не страшно, при желании можно сделать обертку:
    var asyncUnlink = fs.unlink;
    fs.unlink = function(fName, callback){
      if(callback)
        return asyncUnlink(fName, callback);
      return nbUnlink(fName);
    };

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

    Ключевое слово async?

    Если нам все-таки хочется добавить красивых асинхронных вызовов на уровне языка, то, видимо, не обойтись без нового ключевого слова. Понятно, что это уже скорее фантазии: изменение языка — слишком уж вольная вольность, в отличие от изменения среды исполнения. Тем не менее, давайте одним глазком глянем, что могло бы получиться:
    var result1 = async(callback, myAsyncFunc(args, callback)); // long form
    var result2 = async myAsyncFunc(args); // short form
    var result3 = async(cb, createTask(args, cb), function(task){TaskManager.register(task);});

    — Длинная форма: callback — это название переменной, в которую будет заскладирован контекст возврата для передачи в асинхонную функцию
    — Короткая форма: контекст возврата будет добавлен последним аргументом
    — Третий вариант — «вывернутый наизнанку»: возвращаемое значение будет передано в лямбду (регистрируем созданную асинхронную «задачу» в неком «менеджере» — может мы захотим ее отменить?), а в точку вызова вернемся обычным для async способом

    Зачем может быть нужна поддержка на уровне языка? Думаю, только если нам нужно сделать что-нибудь этакое с нашим хитрым колбэком. Например, отдать его в несколько функций (ой, до чего порочная будет практика). В большинстве случаев должно хватать поддержки на уровне библиотечных функций. И уж точно вызовы неблокирующих библиотечных функций будут смотреться лучше, чем засилье ключевого слова async.

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

    Напоследок


    Я надеюсь, что когда-нибудь неблокирующие библиотечные функции будут добавлены в V8 и node.js, сделав их еще асинхроннее и прекраснее. Я надеюсь, что их также добавят и в Python. Я надеюсь, что на этом не остановятся и во всех новых и потенциально любимых языках и средах вместо синхронных функций будут функции неблокирующие — везде, где это имеет смысл.

    * Все исходники в этой статье подсвечены с помощью Source Code Highlighter.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Комментарии 78
    • +9
      поменяйте async на non-blocking, пожалуйста.
      асинхронность это возможность исполнять несколько действий одновременно, что в рамках однопоточного скрипта невозможно, за исключением воркеров, каждый из которых тоже однопоточный.
      • 0
        Ох, война терминологии :). Кстати, практически везде начинает использоваться бренд «async», тут как с HTML5, наверное придётся смириться :).
        • +3
          просто когда я представляю «умение писать асинхронный скрипт с импользованием jQuery» мне становится дурно. но, похоже, что придется смириться.
        • НЛО прилетело и опубликовало эту надпись здесь
          • +3
            Некоторые функции «блокируются»: например, чтение килобайта через медленное соединение может подвесить прогу. В таких случаях нужно уметь делать что-то параллельно, не дожидаясь результата. Да и одноядерные процессоры умеют переключать контекст исполнения между thread'ами и процессами.
            • 0
              >В таких случаях нужно уметь делать что-то параллельно, не дожидаясь результата.
              concurrency != parallelism
        • +10
          Хорошая статья, давно пора на Хабре больше умного писать и меньше перепечатки новостей :)

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

              Не можете подсказать линк?
            • +2
              Насчёт python и неблокирующего нетворкинга смотрим на gevent.org — почти идеальное решение ;-).
              • +2
                Почти. 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).
                • 0
                  3. Отклонения от стандартных интерфейсов Event, Queue, Semaphore. Особенно Event.
                  • 0
                    > Что неправильно:
                    > 1. libevent (надо было libev)

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

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

                    Такая возможность есть — модуль gevent.monkey.
                    • 0
                      1. www.fapws.org/benchmarks

                      2. Спасибо, не знал.
                      • 0
                        Если честно, то цифры в бенчмарке несколько странные.
                          • 0
                            Бенчмарк действительно очень полный, в отличие от приведенного мною. Признаться, не хотел свести все к сравнению питонячих веб-серверов.

                            Вспомнил, что разработчик fapws в 3-й версии отказался от libevent в пользу libev (должно быть не зря). Наверное корректнее было посмотреть бенчмарк fapws2 vs fapws3, хотя на результаты скорее всего сильно повлияет другая база кода.
                  • +3
                    Майкры работают над 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 аж нисколечки нечетабельный.
                    • –1
                      Не совсем честно сравнивать спец. ЯП :), у автора в статье тоже есть примеры, да и новые ЯП со встроенной асинх. давно есть.
                      • 0
                        Это F#, язык общего назначения, с функциональным уклоном. Эти воркфлоу реализованы стандартными средствами языка. Средство называется Computation Expressions.
                        • 0
                          Ну точно так же можно вспомнить Erlang (хотя там в принципе другая идеология) или Go. Смысл статьи показать, как асинхронность можно использовать в широко используемых ЯП.
                      • 0
                        Немного поклобасило код, тут наверное будет удобней смотреть gist.github.com/492365
                        • 0
                          Ну что скажешь, молодцы. Хотя могли бы и элегантнее :-)
                          • 0
                            Да мне тож так кажется. Например восклицательный знак из let прибрать, слово Async. Но фишка в том что это заимплеменчено стандарными сердствами языка. Тоесть теоретически такую же штуку можно провернуть, чтобы запустить этот код на разных машинах. Или например что-бы вставить туда MSMQ. Или сделать его выполняем в браузере (компилится в javascript — WebSharper). Или… масса применений.
                        • –1
                          Erlang прекрасен!
                          • 0
                            В плане асинхронности — да.
                            Но там будут свои геморрои — например, меньше полезных либ, некоторые кривые, поэтому какие-то очевидные вещи, которые на js/python/etc… делаются сходу, здесь потребуют времени.
                            • 0
                              надеюсь в fprog появиться эта статья, а пока можно почитать только это:
                              node.js vs. Erlang — groups.google.com/group/erlang-russian/browse_thread/thread/cf3c65b0c5230834

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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