Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 25.06.2014 | нет комментариев

Обычно в таких статьях делают заголовок вида «аналог 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 Task.Run(() => 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-фреймворке простейшее тестовое приложение из одного поля ввода (многострочного) и одной кнопки. Тогда наш т

 

Источник: programmingmaster.ru

 

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB