заметки на полях (фильтрации)
May. 8th, 2013 01:19 amНа языках программирования можно ввести отношение частичного порядка "А < Б", когда с языка А на язык Б пересесть можно, и это ощущается как прогресс, а вот обратно возвращаться очень неохота и мучительно. Если не ошибаюсь,
lionet в свое время заметил, что в этом отношении заметны две вершины - хаскель и лисп, с их высоты все остальные языки кажутся недостаточно хорошими. Но занятно другое: у этого отношения есть также два дна, по сравнению с которыми все другие языки выглядят превосходными, - это PHP и C++. :)
После ряда других языков мне заставить себя писать что-то на С++ очень сложно, но иногда такая необходимость возникает. Помогает сгладить моральные мучения лишь возможность найти в языке крупицы чего-то хорошего. Нынче вот взялся за новую реализацию своего фирменного super resolution движка, и что меня сейчас радует и выручает, это присутствующие в языке элементы зависимых типов. У меня код оперирует блоками разных размеров и векторами разной точности: это могут быть целые координаты в кадре низкого разрешения, в кадре высокого разрешения, а также координаты с полупиксельной и четвертьпиксельной точностью. Плюсовые шаблоны позволили описать эти вещи как семейства типов, индексированные целочисленными значениями, т.е. натурально зависимые типы получились:
В результате блоки разного размера - это разные типы, и векторы разной точности - разные типы, реально очень помогает не запутаться. Плюс компилятору подспорье: у многих циклов число итераций теперь известно статически, можно хорошо оптимизировать. В иных языках для таких вещей можно использовать phantom types, но там может быть сложнее сделать функцию refine, переводящую вектор на следующий уровень точности, в соседний слой семейства. Все-таки очень удобно, когда с параметром типа можно делать всякую арифметику и использовать его сразу на двух уровнях: типов и выражений.
К слову о зависимых типах. Одна тривиальная мысль о них мне лично оказалась весьма полезной для понимания. Мы привыкли в функциональных языках обозначать тип функции из А в В как А -> B, где А и В какие-то конкретные типы вроде Bool и Int, элементами которых служат значения вроде true и 2. Теперь добавим в систему типов еще один тип, назовем его Type, элементами которого являются типы. Тогда A -> Type будет просто типом функции, которая каждому элементу А сопоставляет какой-то тип. Это и получится зависимый тип, и именно так он и обозначается в соответствующих языках. Например, пишут B : A -> Type и говорят, что В - это зависимый от А тип, или семейство типов, индексированное значениями из А. Но эту же запись можно воспринимать и буквально - как обычную функцию, просто кодомен у нее не совсем обычный.
После ряда других языков мне заставить себя писать что-то на С++ очень сложно, но иногда такая необходимость возникает. Помогает сгладить моральные мучения лишь возможность найти в языке крупицы чего-то хорошего. Нынче вот взялся за новую реализацию своего фирменного super resolution движка, и что меня сейчас радует и выручает, это присутствующие в языке элементы зависимых типов. У меня код оперирует блоками разных размеров и векторами разной точности: это могут быть целые координаты в кадре низкого разрешения, в кадре высокого разрешения, а также координаты с полупиксельной и четвертьпиксельной точностью. Плюсовые шаблоны позволили описать эти вещи как семейства типов, индексированные целочисленными значениями, т.е. натурально зависимые типы получились:
#define VP_LOWRES 1
#define VP_HIRES 2
#define VP_HALF 3
#define VP_QUARTER 4
template <int Prec>
class Vec
{
public:
int x, y;
Vec(int vx, int vy) : x(vx), y(vy) {}
Vec operator+(Vec<int Prec> &a) { return Vec<Prec>(x + a.x, y + a.y); }
Vec operator-(Vec<int Prec> &a) { return Vec<Prec>(x - a.x, y - a.y); }
Vec<Prec + 1> refine() { return Vec<Prec + 1>(x*2, y*2); }
...
};
class Plane
{
...
template<int W> void readBlockQP(Vec<VP_QUARTER> v, MonoBlock<W> &block);
...
}
В результате блоки разного размера - это разные типы, и векторы разной точности - разные типы, реально очень помогает не запутаться. Плюс компилятору подспорье: у многих циклов число итераций теперь известно статически, можно хорошо оптимизировать. В иных языках для таких вещей можно использовать phantom types, но там может быть сложнее сделать функцию refine, переводящую вектор на следующий уровень точности, в соседний слой семейства. Все-таки очень удобно, когда с параметром типа можно делать всякую арифметику и использовать его сразу на двух уровнях: типов и выражений.
К слову о зависимых типах. Одна тривиальная мысль о них мне лично оказалась весьма полезной для понимания. Мы привыкли в функциональных языках обозначать тип функции из А в В как А -> B, где А и В какие-то конкретные типы вроде Bool и Int, элементами которых служат значения вроде true и 2. Теперь добавим в систему типов еще один тип, назовем его Type, элементами которого являются типы. Тогда A -> Type будет просто типом функции, которая каждому элементу А сопоставляет какой-то тип. Это и получится зависимый тип, и именно так он и обозначается в соответствующих языках. Например, пишут B : A -> Type и говорят, что В - это зависимый от А тип, или семейство типов, индексированное значениями из А. Но эту же запись можно воспринимать и буквально - как обычную функцию, просто кодомен у нее не совсем обычный.
no subject
Date: 2013-05-07 11:29 pm (UTC)Концепции и парадигмы — это для архитектурных астронавтов термины, я ими не пользуюсь.
Реализация Linq2SQL появилась в Haskel на 4+ года позже, чем в C#.
>Где здесь элегантность языка просвечивает?
В том и элегантность, что огромную кучу сложной, но необходимой логики спрятали за двумя несложными для понимания ключевыми словами.
>пользователь C# сам эту сложность соорудить не в состоянии
Он в состоянии соорудить практически то же самое, что только отсутствием синтаксического сахара будет отличаться от того как щас сделано.
Просто по объективным причинам объём работы большой.
>ничего общего не имеет с элегантностью языка. Практично? Да. Удобно? Да.
Именно это я и называю элегантностью.
Язык программирования — это не произведение искусства, а инструмент для решения реальных задач.
>В C# тоже с этим не всё так хорошо как в лиспе, например.
Лисп даже не умеет традиционного статически типизированного ООП.
no subject
Date: 2013-05-08 07:38 am (UTC)Язык программирования — это не произведение искусства, а инструмент для решения реальных задач. — с этого я и начинал. Но тем самым вы ограничиваете себя именно кругом «реальных задач», это и есть «зона комфорта» C#, в то время, как более развитые языки (H. и L.) обладают существенной гибкостью, которая позволяет им неплохо себя чувствовать in the wild, в поле, в котором еще никто подобных задач не решал (и откуда и приходят все фичи, которыми так гордится сообщество C#, похоже, не подозревая о существовании хотя бы Microsoft Research, словно их дают незримые майкрософтовские боги — именно это подразумевалось под «пользователь C# сам эту сложность соорудить не в состоянии», у него есть возможность, потенциальность, но откуда возьмутся идеи?). Вы и не подозреваете, сколько прекрасных идей и инструментов все еще нет в C# и не будет.
Лисп даже не умеет традиционного статически типизированного ООП. — Clojure очень даже умеет, поскольку она работает поверх JVM, вот только сами кложуристы избегают тыкать палочкой в goop без особой необходимости. Думаю, соорудить Java-style велосипед вполне возможно и в Common Lisp, но зачем?!
no subject
Date: 2013-05-08 05:54 pm (UTC)Вообще не делается. Вот вам к примеру метод на C#, который принимает 100 байт из сокета, на каждый байт шлёт в ответ XOR всех ранее полученных байт:
async Task DoThat( Socket s ) { byte res = 0; for( int i = 0; i < 100; i++ ) { byte b = await s.ReadByte(); res ^= b; await s.WriteByte( res ); } }Ваше "намного проще" выливается в глобальный стейт, потерянные исключения, занятый поток на время ожидания, неочевидный код когда тривиальный цикл for бьётся на какие-то отдельные слабо связаные друг с другом методы/лямбды/замывания/коллбэки, и/или кучу других минусов.
>позволяет ли async/await эту сложность переиспользовать, комбинировать
Да.
>природно порождать новые конструкции языка?
Нет и оно не нужно, поскольку код с "порождёнными новыми конструкциями языка" крайне сложно читать и поддерживать.
>Вы и не подозреваете, сколько прекрасных идей и инструментов все еще нет в C# и не будет.
Список в студию.
no subject
Date: 2013-05-08 07:28 pm (UTC)но это же вы из non-blocking IO в три строки сделали blocking IO. Как бы, понятно всё, не понятно только, зачем гвозди забивать табуреткой :)
я надеялся увидеть async, делающий map, и await, делающий reduce.
"занятый поток на время ожидания"
во, а вот это вот место я и хотел в другой ветке расспросить. Вот у вас await s.Read(). В этом месте поток, вызывающий метод, делает что? context switch? Поток зелёный или нативный? Если нативный, то получается "занятый поток на время ожидания".
no subject
Date: 2013-05-08 07:53 pm (UTC)Нет не сделал.
Пока await ждёт, ни один поток этим не занят.
Таких одновременно работающих тасков могут быть тысячи на разных сокетах, это вообще не уронит performance, как было бы в случае blocking IO.
>async, делающий map, и await, делающий reduce
async-await не нужен для CPU bound тасков.
map-reduce по-идее элементарно делается на TPL (я думаю надо начать с чтения MSDN по вопросу Parallel.ForEach), но гуглите сами, я этот кусок фреймворка не использовал.
>делает что?
Что угодно ещё на усмотрение task scheduler’а.
Иногда например (если данные уже в буфере сокета, а работающих тасков меньше чем ядер) он сразу вернёт байт и продолжит в этом же таске.
Иногда переключится на другой таск.
Иногда уснёт до появления или новых тасков которые нужно исполнять или данных в сокете.
no subject
Date: 2013-05-09 07:17 am (UTC)no subject
Date: 2013-05-09 08:15 am (UTC)no subject
Date: 2013-05-09 08:48 am (UTC)int blah = async compute();
В месте вызова compute() кто-то должен сохранить состояние функции-вызыватора. Куда? Без async всё складывается в железный стек. С async всё, что лежит на стеке, должно уйти куда-то ещё - в мягкий стек; мы же хотим, чтобы железный поток не простаивал, а занимался делом.
1. Поскольку мы хотим смешивать async и не async вызовы, стек не может быть железным и в случае не async вызовов. Поток должен вести мягкий стек. Совокупность мягких стеков и есть совокупность зелёных ниток. Речь идёт о том, что якобы зелёные нитки вести дешевле, чем железные? Выгоды преувеличены.
2. Раньше было:
compute();
finish();
Теперь кто-то эти методы может вызвать async. Раньше у вас были головняки со state transition гарантированным program order, теперь у вас головняки с concurrency in practice, где ничего такого не было. Как пример, запись в поток в цикле - речь не обязательно о файловом потоке, подумайте о шифровании или сжатии. Если функция-вызыватор сама не позаботится о гигиене использования потока, вместо нормального цикла получится асинхронный ад с out of order заполнением потока.
3. async Task<blah> oops() { async oops() } - интересно, что переполнится в этом случае и какова диагностика? Тут ещё вариант, что я не понял, как правильно выразить эту мысль - но должно же быть возможно выразить бесконечную рекурсию. Диагностика этого не то, чтобы невозможна, но хотелось бы услышать, как это решается в данном езыке.
no subject
Date: 2013-05-09 09:19 am (UTC)В кучу уходит, не в стек.
>кто-то эти методы может вызвать async
Этот кто-то .NET framework.
>теперь у вас головняки с concurrency in practice
Я написал довольно много async-await кода (начинал году в 2010 с Async CTP). Этот подход как раз решает эти ваши "головняки с concurrency in practice", а не создаёт. Очень часто удаётся написать даже очень нетривиальный параллельный код без единой блокировки, только с использованием гарантий, которые дают компилятор и рантайм.
>Если функция-вызыватор сама не позаботится о гигиене использования потока
Ничего не нужно делать.
>вместо нормального цикла получится асинхронный ад с out of order заполнением потока.
Не получится, за вас framework это сделает. Framework гарантирует, что statements в async методе будут исполняться строго последовательно, хотя и не обязательно одним потоком конечно.
А может быть и одним потоком, поведение зависит от того есть ли synchronization context, framework сам умеет при необходимости маршалить вызовы в тот поток, из которого вызвали await.
>async Task oops() { async oops() }
Имеете в виду видимо
async Task oops() { await oops(); }?
Получите обычный stack overflow exception в рантайме.
>должно же быть возможно выразить бесконечную рекурсию
Элементарно.
async Task oops() { await Task.Yield(); await oops(); } - ничего не будет падать, и получите бесконечную рекурсию, только зачем?
no subject
Date: 2013-05-09 12:12 pm (UTC)Вот это и есть мягкий стек. В кучу должны складывать все - и вызыватор, и вызываемый.
"Этот подход как раз решает эти ваши "головняки с concurrency in practice", а не создаёт."
я думаю, моя мысль ускользнула. Пример на гугле показывает как раз головняки с concurrency in practice, а ваш пример с await s.readByte() ничего интересного не делает. Интересное как раз вот в чём (по памяти пример из гугла):
Task<string> getString = connection.getStringAsync("http://..."); -- здесь, я так понимаю, не только async таск создаётся, но и запускается. это fork
DoSomethingIndependent(); -- Вот это вот и есть concurrency in practice
return await getString; -- это join
Вызыватор должен быть в курсе, где там внутри методов есть async, иначе getStringAsync и DoSomethingIndependent напорются на конкурентность, где её не было. Проблемы нет, если SomethingIndependent действительно independent.
"Получите обычный stack overflow exception в рантайме."
Прекрасно. А теперь внутри getStringAsync вызывается await функции, вызывающей кого-то ещё и т.д. Как диагностировать, что вызвало stack overflow exception?
no subject
Date: 2013-05-09 12:33 pm (UTC)Нет, вызываемый тупо возвращает экземпляр Task. Он может быть даже выполнен синхронно уже. Таск вовсе не обязан быть реализован через async метод, он может работать как угодно ещё.
Сам async метод - да, складывает в кучу конечно, но это автомагически делает для вас компилятор.
>создаётся, но и запускается. это fork
Нет никаких форков. Все Winapi давным-давно асинхронные, в WinRT вон вообще убрали API блокирующий IO, шоб неповадно было.
>Вызыватор должен быть в курсе, где там внутри методов есть async
Тот кто зовёт вообще не в курсе как именно реализовано API. Для него это самый обычный метод, возвращающий объект Task.
>иначе getStringAsync и DoSomethingIndependent напорются на конкурентность, где её не было
Ещё раз повторяю. Фреймворк by design гарантирует исполнение вашего async-метода не более чем одним потоком одновременно (если вы конечно его только один раз позвали).
>Как диагностировать, что вызвало stack overflow exception?
Точно так же как до появления асинков, посмотреть в дебаггере на call stack.
no subject
Date: 2013-05-09 06:57 pm (UTC)И вон в параллельной ветке упоминают pool, который async таски подбирает.
Про fork - это термин.
(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:no subject
Date: 2013-05-08 08:33 am (UTC)Ну ок, от lazy val в Скале отличается, видимо, тем, что апликативный порядок, а вот за Х-ь интересно.
no subject
Date: 2013-05-08 06:08 pm (UTC)Простите шо вынужден послать вас в google но это как раз тот случай когда это необходимо.
Дофига всего делает.
>запускается на каждый async новый поток?
Зависит главным образом от того, кто реализовывал метод, который ждёте.
Или если запускаете новый таск путём вызова Task.Run, от того, указали ли флажок TaskCreationOptions.LongRunning (да и то это просто hint для scheduler'а).
В большинстве случаев ответ "нет, никаких новых потоков не запускается, при необходимости используются потоки, имеющиеся в thread pool".
>fork/join есть?
Не понял вопрос.
Есть Task.Run(), Task.WhenAny(), Task.WhenAll(), Task.ContinueWith() и множество других API разной степени полезности.
>Кто и как решает, достойно ли вычисление выполнять async (даже если оно декларировано async)?
Код метода до первого await - вы решаете. Остальной код метода - решает в основном специальный scheduler, но в 95% случаев он работает как надо без усилий с вашей стороны, в оставшихся 5% требуются тривиальные действия шоб это указать.
>И чем это отличается от
async-await имеет совсем мало общего с ленивыми вычислениями. Я не знаю X, но почти уверен что ответ "всем отличается".
no subject
Date: 2013-05-08 06:27 pm (UTC)То, как вы описываете эту фичу, похоже на джаву, где тоже есть fork / join со всякими примочками. Но это не то. Здесь программисту нужно вручную размечать, какие кусочки следует выполнять параллельно.
В Х-е есть разделение задачи на кусочки и параллельное их вычисление, с той огромной разницей, что "оно само". я не настолько крут, чтобы подробно объяснить, как оно именно в Х-е сделано, но как аргумент в пользу языковых фич сгодится. В Пласмейере обсуждается технология - как там strictness analysis и лень позволяют параллелизовать программу как она есть.
no subject
Date: 2013-05-08 06:41 pm (UTC)Стало в разы проще писать и отлаживать эффективный асинхронный параллельный I/O-bound код.
"Эффективный" = минимальный CPU usage при заданном throughput (особенно актуально на смартфонах где CPU usage=батарейка), или максимальный throughput (особенно актуально на серверах).
Для параллельного CPU-bound кода async-await не нужен, нужен TPL.
Но я его не использовал поскольку не было нужды.
И насколько я понимаю, аналоги разной степеени годности нынче есть во всех языках, начиная с C++.
async/await делает что?
Date: 2013-05-09 09:53 am (UTC)В .NET пару версий назад появился TPL (Task Parallel Library) — раздел стандартной библиотеки, крутящийся вокруг класса
Task<TResult>. Этот класс представляет из себя то, что в теории называется «фьючерка» (калька с английского ввиду незнания русского аналога). Создание таких задач абстрагирует от манимуляций с конкретными потоками. При простейшей форме использования для задач берутся потоки из пула, но такие детали при необходимсти можно настраивать.Так вот, это и есть способ выполнения того, что, ты говоришь, можно делать библиотечными средствами в других языках.
Что происходит в примерах, приводимых выше soonts? Асинхронность заключается в том, чтобы:
1) указать, что надо сделать параллельно,
2) указать, что доделывать после того, как параллельная часть будет выполнена,
3) вернуть управление.
То есть тот самый CPS на замыканиях, изменений языка для этого делать нет необходимости, можно обойтись библиотечными средствами старого C#. Теперь к твоему вопросу.
> я не понял, async/await делает что?
asyncне делает ровным счётом ничего; тут важно, что делаетawait. Он позволяет автоматизировать составление замыкания, используемого в CPS. При этом производит проброску исключений из фьючерки в вызывающий поток, предоставляет механизм кооперативной отмены (а не дикое киляние потока), по умолчанию сохраняет контекст синхронизации (если фьючерка была порождена в UI-потоке, то из continuation'а после task'а можно менять UI без хитрой диспетчеризации), etc. Плюсawaitимеет очень удобный синтаксис; конечно, можно переписать функцию наTask'ах, через которое всё и работает, фактически без изменения вызывающей стороны — как это выглядело бы в аналогичных API других языков и платформ. Но, скажем, если дело происходит в цикле, то надо в continuation протаскивать состояние обхода цикла, в цепочку continuation'ов передавать необходимость выполнить следующую итерацию, и прочий геморрой с исключениями. C# 5.0 же позволяет делать это автоматически, генерируя нужный boilerplate code из синтаксиса, который внешне похож на обычный синхронный, и отлаживается тоже просто.Re: async/await делает что?
Date: 2013-05-09 11:09 am (UTC)CPS автоматом - тоже интересно, но возникают вопросы, как там с интеропом с кодом с другими call conventions - смесь async и не async, как используется нативный стек и как preserve program order, если внезапно наследники сделают куски кода async. Мне кажется, это концептуальная проблема, потому очень интересно, как этот вопрос решается или ограничивается круг проблем.
Re: async/await делает что?
Date: 2013-05-09 11:34 am (UTC)На все вопросы не отвечу, просто в общих чертах. С точки зрения пользователя некоего асинхронного API: он видит методы, которые возвращают
TaskилиTask<TResult>, вполне себе традиционно. Если автор API решит внутри реимплементировать эти методы через новые языковые фичи и новые асинхронные методы стандартной библиотеки, то внутри у него где-то появитсяawait, и компилятор обяжет его пометить метод словомasync. Но! Частью сигнатуры метода это слово не является, то есть для пользователя метода осталось всё по-прежнему (не произошло ломающего изменения), он может использовать возвращаемыйTaskпо старинке, или await'ить его по-модному. Т.е.asyncв объявлении метода, возвращающего таск, — это указание компилятору, как компилировать содержимое метода (упрощённо говоря, часть «плоского кода» послеawaitвкомпилируется в замыкание-continuation), но не навязывает требования изменения способа вызова. Собственно, этот флажокasyncв сигнатуре метода не принципиален, компилятор теоретически сам может внутри определения метода увидетьawaitи сгенерировать соответствующий код. Простоawaitрешено было сделать контекстным ключевым словом (в том числе для обратной совместимости с кодом, который использовалawaitпросто как обычный идентификатор), и чтоб отличать его вхождение в код метода как инструкцию, добавили требование пометить этот метод словомasync. Т.е. это слово учитывается компилятором при генерации кода метода, но не учитывается при генерации кода использования метода.Re: async/await делает что?
Date: 2013-05-09 11:52 am (UTC)Re: async/await делает что?
Date: 2013-05-09 12:16 pm (UTC)Как обычно у Microsoft, всё прекрасно с интеропом.
Завернуть "старый" async API (который BeginXxx/EndXxx) в новый с тасками — одна строчка кода.
Блокировать тред дожидаясь таска — тоже одна, правда если есть контекст синхронизации, возможен deadlock. Да и вообще треды не нужно блокировать, они созданы быть свободными.
Синхронизировать два таска, не завершая ни один из них, чуть больше одной строчки но меньше 10 (можно например создать экземпляр класса TaskCompletionSource, потом в одном TaskCompletionSource.SetResult, во втором await TaskCompletionSource.Task).
Re: async/await делает что?
Date: 2013-05-09 06:59 pm (UTC)Re: async/await делает что?
Date: 2013-05-09 11:51 am (UTC)>автоматизировать составление замыкания, используемого в CPS
async-await вообще не использует замыканий, замыкания не позволяют например прыгнуть в середину цикла.
Вместо замыканий генерируется internal class, который хранит стейт (включающий помимо прочего аргументы async метода, и локальные переменные в нём).
И ещё он реализует интерфейс IAsyncStateMachine.
Вот как выглядит главный метод MoveNext этого internal класса для моего примера выше про 100 байт и XOR, после небольшого рефакторинга для читаемости:
void MoveNext() { try { switch( this._state ) // Jump to the middle of the method based on the state. { case 0: goto Label_0067; case 1: goto Label_00F4; } // The state was -1 => run from the start. this.res = 0; this.i = 0; while( this.i < 100 ) { TaskAwaiter<byte> local1 = this.socket.ReadByte().GetAwaiter(); if( local1.IsCompleted ) goto Label_0085; // If the await completed synchronously, the task continues to run on the same thread. this._state = 0; this.awaiter4 = local1; this._builder.AwaitUnsafeOnCompleted<TaskAwaiter<byte>, Program.DoThatInternalClass>( ref local1, ref this ); return; Label_0067: // Came here because the first await has completed. local1 = this.awaiter4; this.awaiter4 = new TaskAwaiter<byte>(); this._state = -1; Label_0085: // This label is not an entry point. local1 = new TaskAwaiter<byte>(); this.b = local1.GetResult(); this.res = (byte)( this.res ^ this.b ); TaskAwaiter local4 = this.socket.WriteByte( this.res ).GetAwaiter(); if( local4.IsCompleted ) goto Label_0113; // If the await completed synchronously, the task continues to run on the same thread. this._state = 1; this.awaiter5 = local4; this._builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.DoThatInternalClass>( ref local4, ref this ); return; Label_00F4: // Came to this method because the 2-nd await has completed. local4 = this.awaiter5; this.awaiter5 = new TaskAwaiter(); this._state = -1; Label_0113: // This label is not an entry point. local4.GetResult(); local4 = new TaskAwaiter(); this.i++; } } catch( Exception ex ) { this._state = -2; // Marshall the exception to the caller. this._builder.SetException( ex ); return; } this._state = -2; // Awake the caller because the method was completed successfully. // If the synchronization context permits, this will happen on this same thread, right from the inside of SetResult() method. this._builder.SetResult(); }Re: async/await делает что?
Date: 2013-05-09 11:54 am (UTC)Это я и назвал замыканием. Замыкание-в-широком-смысле, если угодно. Захват контекста.
> async-await вообще не использует замыканий, замыкания не позволяют например прыгнуть в середину цикла.
Да, я как раз про цикл упомянул выше как пример, когда простой код будет сложно переписать на продолжениях.
await
Date: 2013-05-09 09:22 am (UTC)Или даже за одним словом —
await.asyncдекоративный фактически.