Техника написания аналога await/async из C# для C++

от автора

Обычно в таких статьях делают заголовок вида «аналог await/async для C++», а их содержимое сводится к описанию ещё одной библиотеки, выложенной где-то в интернете. Но в данном случае нам не требуется ничего подобного и заголовок точно отражает суть статьи. Почему так смотрите ниже.

Предыстория

Все примеры кода из этой статьи были придуманы мною для аргументации в одном из «классических» споров вида «C# vs. C++» на одном форуме. Спор закончился, а код остался, и я подумал почему бы не оформить это в виде нормальной статьи, которая послужила бы входной на Хабре. Вследствие таких исторических причин, в статье будет много сравнений C# и C++ подходов.

Постановка задачи — асинхронное программирование

Весьма часто в работе встаёт задача произвести какие-то действия в отдельном потоке и потом обработать результат в изначальном (обычно UI) потоке. Это одна из разновидностей так называемого асинхронного программирования. Это задача хорошо известная и имеет множество различных решений в большинстве языков программирования. Например в C++ это может выглядеть так:

auto r=async(launch::async, [&]{return CalcSomething(params);}); DoAnother(); ProcessResult(r.get());//get - блокирующая 

для схемы с блокировкой вызывающего потока. Или так:

auto r=async(launch::async, [&]{return CalcSomething(params);}); while(r.wait_for(chrono::seconds(0))!=future_status::ready) DoAnother(); ProcessResult(r.get()); 

с опрашивающей схемой. Ну а для UI потоков вообще проще всего воспользоваться уже работающим циклом и сделать уведомляющую схему:

thread([=]{PostMessage(CalcSomething(params));}).detach(); ... OnDataMessage(Data d){ProcessResult(d.get<type>());} 

Как видно ничего особо сложного тут нет. Это код на C++, а скажем на C# всё запишется буквально так же, только вместо thread и future будет Thread и Task. Но у последнего варианта есть один небольшой минус: код вычисления и код обработки находятся в разных контекстах (и могут находиться даже в разных файлах исходников). Иногда это даже полезно для более строгой архитектуры, но ведь всегда хочется поменьше писанины… В последних версиях C# появилось любопытное решение.

C# реализация

В последних версиях C# мы можем написать просто:

private async void Handler(Params prms) {     var r = await new Task(() => CalcSomething(prms););     ProcessResult(r); } 

Для тех кто не в курсе, поясню, как здесь происходит последовательность вызовов. Предположим что функция Handler вызвана из UI потока. Возврат из функции Handler происходит сразу после запуска асинхронной задачи CalcSomething. Далее, она выполняется параллельно UI потоку, а после её завершение и когда UI поток освободится от своих текущих задач, он выполнит ProcessResult с данных полученными из второго потока.

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

Сопроцедуры

Сопроцедура по простому — это блок кода с множественными точками входа. Применяются они чаще всего для случаев очень большего числа параллельных задач (например в реализации сервера), где наличие подобного числа потоков уже совершенно неэффективно. В таком случае они позволяют создать видимость потоков (кооперативная многозадачность) и этим сильно упрощают код. Так же с помощью сопроцедур можно реализовывать так называемые генераторы. Реализация сопроцедур бывает как встроенная в язык, так и в виде библиотеки и даже предоставляемая ОС (в Windows сопроцедуры называются Fiber).

В C# же сопроцедуры применили не для таких классических целей, а для реализации любопытного синтаксического сахара. Реализация у нас тут встроенная в язык, но при этом далеко на самая лучшая. Это так называмя stackless реализация, которая по сути представляет собой конечный автомат хранящий в себе нужные локальные переменные и точки входа. Именно из этого следует большая часть недостатков C# реализации. И необходимость расставлять «async» по всему стеку вызова и лишние накладные расходы автомата. Кстати, await — это не первое появление сопроцедур в C#. yield — это тоже самое, только ещё более ограниченное.

А что у нас в C++? В самом языке нет никаких сопроцедур, но существует множество различных реализаций в виде библиотек. Есть она и в Boost’e, причём там реализован как раз самый эффективный вариант — stackfull. Он работает через сохранение/востановление всех регистров процессора и стека соответственно — по сути как у настоящих потоков, только это всё без обращения к ОС, так что практически мгновенно. И как всё в Boost’e, оно отлично работает на разных ОС, компиляторах, процессорах.

Ну что же, раз в C++ у нас имеется даже более мощная реализация сопроцедур чем в C#, то просто грех не написать свой вариант await/async синтаксического сахара.

C++ реализация

Посмотрим что нам даёт библиотека Boost.Coroutine. Первым делом нам надо создать экземпляр класса coroutine, передав ему в конструкторе нашу функцию (функтор, лямбда-функцию), причём у этой функции должен быть один (может быть и больше, уже для наших целей) параметр, в который будет передан специальный функтор.

using Coro=boost::coroutines::coroutine<void()>; Coro c([](Coro::caller_type& yield){     ...     yield();//прерывает выполнение     ...     yield();//прерывает выполнение     ... }); ... c();//исполнение нашей функции с точки последнего прерывания 

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

Теперь у нас есть всё что нужно. Осталось применить немного магии шаблонов и макросов (это только чтобы было внешне совсем похоже на C# вариант) и получаем:

using __Coro=boost::coroutines::coroutine<void()>; void Post2UI(const void* coro); template<typename L> auto __await_async(const __Coro* coro, __Coro::caller_type& yield, L lambda)->decltype(lambda()) { 	auto f=async(launch::async, [=](){ 		auto r=lambda(); 		Post2UI(coro); 		return r; 	}); 	yield(); 	return f.get(); } void CallFromUI(void* c) { 	__Coro* coro=static_cast<__Coro*>(c); 	(*coro)(); 	if(!*coro) delete coro; } #define async_code(block) { __Coro* __coro=new __Coro; *__coro=__Coro([=](__Coro::caller_type& __yield){block});} #define await_async(l) __await_async(__coro, __yield, l) 

Вся реализация занимает какие-то жалкие 20 строчек простейшего кода! Их конечно можно засунуть в отдельный hpp файл и обозвать чем-то типа библиотеки, но это будет просто смешно. Правда нам требуется определить ещё пару строк, уже зависящих от выбора нашего GUI-фреймворка (или вообще нативного api). Что-то типа:

void Post2UI(const void* coro) {PostMessage(coro);} void OnAsync(Event& event) {CallFromUI(event.Get<void*>());} 

Но это всего пара строк, одна на всё приложение и одинаковая для всех приложений на одном фреймворке. После этого мы сможем легко писать такой код:

void Handler(Params params) async_code (     auto r = await_async([&]{return CalcSomething(params);});     ProcessResult(r); ) 

И последовательность вычислений будет в точности как в C# варианте. Причём нам не пришлось менять сигнатуру функции (добавлять async по всему стеку вызова) как в C#. Более того, здесь мы не ограничены запуском одной асинхронной задачки на функций. Мы можем запустить на параллельное исполнение сразу несколько асинхронных блоков или вообще пройтись в цикле. Например такой код:

void Handler(const list<string>& urls) {     for(auto url: urls)  async_code     (         result+=await_async([&]{return CheckServer(url);});     ) } 

запустит параллельное выполнение CheckServer для каждого элемента в списке и соберёт все результаты в переменной result. Причём очевидно что никакой синхронизации, блокировок и прочего не требуется, т.к. код result+=… будет исполняться только в UI потоке. В C# такое естественно тоже без проблем записывается, но надо делать ещё отдельную функцию, которую и вызывать в цикле.

Тестирование

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

class MyWindow: public Window { 	void TestAsync(int n) async_code 	( 		output<<L"Запускаем асинхронное из потока "<<this_thread::get_id()<<'\n'; 		auto r=await_async([&]{ 			this_thread::sleep_for(chrono::seconds(1)); 			wostringstream res; 			res<<L"Завершена работа в потоке "<<this_thread::get_id()<<L" над данными "<<n; 			return res.str(); 		}); 		output<<L"Показываем результат в потоке "<<this_thread::get_id()<<L": "<<r<<'\n'; 	)	 	void OnButtonClick(Event&) 	{ 		TestAsync(12345); 		TestAsync(67890); 		output<<L"Показываем MessageBox из потока "<<this_thread::get_id()<<'\n'; 		MessageBox(L"Тест!"); 		output<<L"MessageBox закрыт в потоке "<<this_thread::get_id()<<'\n'; 	} 	Editbox output; };  class MyApp : public App { 	virtual bool OnInit() 	{ 		SetTopWindow(new MyWindow); 		return true; 	} 	void OnAsync(Event& event)  	{ 		CallFromUI(event.Get<void*>()); 	} }; void Post2UI(const void* coro) { 	GetApp().PostMessage(ID_ASYNC, coro); } 

MessageBox стоит для проверки работы с модальными окнами. Полученный результат:

Запускаем асинхронное из потока 1
Запускаем асинхронное из потока 1
Показываем MessageBox из потока 1
Показываем результат в потоке 1: Завершена работа в потоке 2 над данными 12345
Показываем результат в потоке 1: Завершена работа в потоке 3 над данными 67890
MessageBox закрыт в потоке 1

Итоги

Думаю что теперь уже не надо объяснять замечание в начале статьи насчёт библиотек. Обладая современным инструментарием (C++11, Boost) любой C++ программист способен за несколько минут и десяток строчек кода написать себе полноценную реализацию await/async из C#. Причём эта реализация будет ещё и гибче (по несколько async блоков на функцию), удобнее (не надо размножать async по стеку вызова) и намного эффективнее (в смысле накладных расходов).

Литература

1. en.cppreference.com/w/cpp/thread — поддержка многопоточности в стандартной библиотеке.
2. www.boost.org/doc/libs/1_54_0/libs/coroutine/doc/html/index.html — реализация сопроцедур в Boost’е.

ссылка на оригинал статьи http://habrahabr.ru/post/185706/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *