Как стать автором
Обновить

Комментарии 26

Годный входной пост. :) Библиотечные штуки весьма полезно писать самому, просто чтобы понять получше как оно работает.
Получилось неплохо, но вы немного путаете. У вас получился не async/await, те немного для другого. Вы правильно пишете, что
часто в работе встаёт задача произвести какие-то действия в отдельном потоке и потом обработать результат в изначальном (обычно UI) потоке
В .NET для этого существовало средство и до async/await, это ThreadPool + SyncronizationContext. Именно последний отвечает за то, чтобы (опять же, цитата)
и когда UI поток освободится от своих текущих задач, он выполнит ProcessResult с данных полученными из второго потока
. Просто применение async/await никакого переключения в исходный поток не делает
В .NET для этого существовало средство и до async/await, это ThreadPool + SyncronizationContext. Именно последний отвечает за то, чтобы (опять же, цитата)

Конечно) И даже не только это средство. Я и написал об этом в начале, что полный набор инструментов для асинхронного программирования есть во многих языках и давным давно. Но это всё без вскусного синтаксического сахара… )))
Просто применение async/await никакого переключения в исходный поток не делает

Тогда в каком потоке по вашему будет исполняться ProcessResult из этого
private async void Handler(Params prms)
{
    var r = await new Task(() => CalcSomething(prms););
    ProcessResult(r);
}

примера? Считаем что Handler вызвали из UI потока.
Ну, в том виде, в котором вы привели код, оно вовсе не будет компилировать.
Однако, предположим, что мы устраним синтаксические ошибки, а так же добавим тип возвращаемого значения из CalcSomething. Получим

private async void Handler(Params prms)
{
    var r1 = await new Task<object>(() => CalsSomething())
    ProcessResult(r1);
}

В таком виде ProcessResult никогда не будет вызван, потому что задачу, отвечающая за CalcSomething, никто никогда не стартует.
Ок, перепишем, как надо

private async void Handler(Params prms)
{
    var r2 = await Task.Run(() => CalsSomething());
    ProcessResult(r2);
}

Ое, задача стартовала, ProcessResult вызван. Теперь самое интересное.

Если у входного потока был установлен SyncronizationContext, или в параметрах Task.Run был передан вот такой аргумент TaskScheduler.FromCurrentSynchronizationContext(), то по окончании исполнения CalсSomething исполнение неявного коллбека с ProcessResult будет добавлено в очередь к этому SyncronizationContext-у, и исполнено на входном потоке.
Если нет — исполнение ProcessResult будет выполнено на первом попавшемся треде, не обязательно UI.
Так вот, у WinForms у входного потока SyncronizationContext есть, поэтому ProcessResult отсылается исполняться на UI.

Если интересно почитать, то вот есть удовлетворительная тематическая ссылка programmers.stackexchange.com/questions/114605/how-will-c-5-async-support-help-ui-thread-synchronization-issues
Промахнулся с ответом — он ниже ушёл. )
Прикол в том, что Async сам по себе вообще не создает отдельных тредов.

The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active. You can use Task.Run to move CPU-bound work to a background thread, but a background thread doesn't help with a process that's just waiting for results to become available.

msdn.microsoft.com/en-us/library/vstudio/hh191443.aspx
Ну так всё правильно. Await/async — это по сути реализация сопроцедур и здесь мы видим описание кооперативной многозадачности. Теоретически через них можно было бы даже попробовать порешать классические задачи сопроцедур… Но с учётом их не особо эффективной реализации в C# наверное всё же нет смысла.
Сделал небольшое дополнение к статье. )))
Ну, в том виде, в котором вы привели код, оно вовсе не будет компилировать.

Ай, не то скопировал… Да и в статье тоже самое… Ужас какой. )))

Если у входного потока был установлен SyncronizationContext, или в параметрах Task.Run был передан вот такой аргумент TaskScheduler.FromCurrentSynchronizationContext(), то по окончании исполнения CalсSomething исполнение неявного коллбека с ProcessResult будет добавлено в очередь к этому SyncronizationContext-у, и исполнено на входном потоке.
Если нет — исполнение ProcessResult будет выполнено на первом попавшемся треде, не обязательно UI.
Так вот, у WinForms у входного потока SyncronizationContext есть, поэтому ProcessResult отсылается исполняться на UI.

Я в курсе) И я как раз поэтому везде и указывал что Handler вызываем из UI потока — тогда он точно туда и вернётся (если не укажем иного, а оно нам и не надо). При вызове же из не UI потока ситуация уже естественно совсем другая, но в таком случае на мой взгляд уже и вообще вся эта схема не требуется, т.к. в потоках без цикла сообщений мы свободны использовать более просты и удобные методы (см. самое начало статье).
Чем людям std::future и std::async не хватает?
Там then у future еще нет во первых, хотя в бусте последнем уже реализовали. Во вторых даже если then будет await все равно намного удобней писать чем .then(лямбда), особенно если код внутри циклов и условий.
ок, then — аргумент. Даже удивляюсь почему его в стандарте нету. Но в бусте есть, и в PPL есть. А чем await удобнее?
then же работает только как однократное поствыполнение. А с await у нас произвольная сложность, т.е. это просто обобщение then до полного подобия линейного кода. Вот смотрите пример:
void Handler(const URL url) async_code
(
    log_window+="Downloading "+url;
    auto xml=ParseXML(await_async([&]{return Download(url);}));
    log_window+="Downloading "+xml.GetValue("/update/text/url ");
    result_window+=await_async([&]{return Download(xml.GetValue("/update/text/url "));});
)

В нём такая последовательность исполнения:
1. «log_window+= в UI потоке и потом возврат из функции Handler
2. Download(url) в отдельном потоке
3. ParseXML и log_window+= снова в UI потоке
4. Download(xml.GetValue(»/update/text/url ")) в отдельном потоке
5. result_window+= снова в UI потоке.

Теперь представьте себе как некрасиво выглядел бы подобный код на вложенных then'ах.
Ну, они ведь не вложенные на самом деле:

task<int> t([]()
{
    return 1;
}).then([](int n)
{
    return n+1;
}).then([](int n)
{
    return n+1;
}).then([](int n)
{
    return n+1;
})

А где там запуск новых потоков? ) Вложенность же от них идёт…

Ну и в любом случае, а если нам if или вообще for понадобится? )

Да и вообще, я же ещё в самом самом начале статьи написал что в принципе у нас давно есть полный набор инструментов для написания асинхронного кода. Т.е. формально мы можем записать что угодно (и даже без then ещё) давным давно. Так что вопрос остаётся только в простоте и удобстве кода — по сути синтаксическом сахаре.
>ок, then — аргумент. Даже удивляюсь почему его в стандарте нету.

Он есть в С++14. Как только его предложили в стандарт буст его и реализовали.

> А чем await удобнее?

Герб Саттер объясняет в этом видео channel9.msdn.com/Events/Build/2013/2-306 на 00:51:08
Вот лучше бы они добавили в C++14 полноценные сопроцедуры! Они мощнее — позволяют реализовать и любой await и ещё много всего другого интересного.
Stackful-реализация сопроцедур может привести к ряду проблем, если в цепочке вызовов есть какой-то код, который об этом не подозревает, при этом используя вещи наподобие обработки исключений. Или я ошибаюсь?
Boost реализация сопроцедур корректно перекидывает исключение в родительский стек: www.boost.org/doc/libs/1_54_0/libs/coroutine/doc/html/coroutine/coroutine.html#coroutine.coroutine.exceptions_in__emphasis_coroutine_function__emphasis_
Кстати, вы в сторону Mono.Tasklets не смотрели? Насколько я понимаю, реализован тот же функционал с сопрограммами, по крайней мере код, похожий на async/await (т. е. без лямбд) на них можно было делать задолго до C# 5. Единственное, требуется поддержка со стороны рантайма, предоставляемая только Mono, т. е. решение непереносимое.
Первый раз вижу эту ссылку, но судя по описанию в ней, это как раз самые классические сопроцедуры, причём не такие ограниченные как await/async, а больше похоже на Boost'ие. Это в смысле сценария использования. А в смысле внутренней реализации надо смотреть уже более подробное описание/исходники.
Я не c++ разработчик, и сильно спотыкаюсь при чтении кода в статье, поэтому лучше спрошу, чем буду пытаться самостоятельно.

У вас в примере все async_code функции ничего не возвращают, это случайность или ограничение данного подхода?
Ээээ, async_code — это не функции, а блоки асинхронного кода. Аналогом этого является всё тело async функции (причём самой первой по стеку вызова) в C# варианте. Возвращать из них какое-то значение (сразу) не имеет никакого смысла, т.к. ещё ничего не вычислено. Поэтому в моей реализации никаких возвратов нет, хотя реализация сопроцедур в Boost и поддерживает такое. Если же речь идёт о том, что бы возвращать значение после отработки всего асинхронного кода, то это просто означает что и предыдущая по стеку вызовов функция должна быть асинхронной и соответственно надо просто переставить asyn_code в неё.
Я пишу на с++ уже более 5 лет, несколько успешных проектов закончены, но когда я читаю фразу:
Вся реализация занимает какие-то жалкие 20 строчек простейшего кода!

мне становиться страшно и не зря… Где вы там увидели простой код, да там через строчку меня повергает в размышления… И когда вижу преобразования в коде, где из void* static_cast-ом получают то, что хотят, может я и не прав и так делать можно, но я бы хотел, чтобы на такие преобразования компилятор выдавал ошибку.
По поводу cast'а… В данном случае мы передаём свои данных (в виде указателя) через очередь сообщений ОС — типизацию при этом сохранить невозможно в принципе.

Да, и кстати, все наши красивые и формальные ООП библиотеки внутри работают именно через подобные cast'ы, потому что иначе работать с функциями ОС невозможно.
Ну тут все просто: 5 лет для C++ это довольно мало. Еще лет через 5-7 разработки на C++, код покажется действительно простым.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации