Usually in such articles they make a heading like “analogue await / async for C ++”, and their content comes down to the description of another library laid out somewhere on the Internet. But in this case, we do not need anything like this and the title accurately reflects the essence of the article. Why so see below.
Prehistory
All code samples from this article were invented by me for argumentation in one of the “classical” disputes of the form “C # vs. C ++ in one forum. The dispute ended, and the code remained, and I thought why not arrange it in the form of a normal article, which would serve as input on Habré. Due to such historical reasons, there will be many comparisons of C # and C ++ approaches in the article.
Problem Statement - Asynchronous Programming
Very often in the work there is a task to perform some actions in a separate thread and then process the result in the original (usually UI) thread. This is one of the varieties of the so-called asynchronous programming. This task is well known and has many different solutions in most programming languages. For example in C ++ it might look like this:
auto r=async(launch::async, [&]{return CalcSomething(params);}); DoAnother(); ProcessResult(r.get());
for a caller blocking scheme. Or so:
auto r=async(launch::async, [&]{return CalcSomething(params);}); while(r.wait_for(chrono::seconds(0))!=future_status::ready) DoAnother(); ProcessResult(r.get());
with a polling scheme. Well, for UI threads in general, the easiest way is to use the already running cycle and make a notification scheme:
thread([=]{PostMessage(CalcSomething(params));}).detach(); ... OnDataMessage(Data d){ProcessResult(d.get<type>());}
As you can see, there is nothing particularly difficult here. This is C ++ code, but let's say in C # everything will be written in the same way, only instead of thread and the future there will be Thread and Task. But the last option has one small minus: the computation code and the processing code are in different contexts (and can even be in different source files). Sometimes it is even useful for more stringent architecture, but you always want less scribbling ... In recent versions of C #, a curious solution has appeared.
')
C # implementation
In recent versions of C #, we can write simply:
private async void Handler(Params prms) { var r = await Task.Run(() => CalcSomething(prms)); ProcessResult(r); }
For those who do not know, I will explain how the sequence of calls takes place here. Suppose the Handler function is called from a UI stream. The return from the Handler function occurs immediately after the launch of the asynchronous CalcSomething task. Further, it runs in parallel with the UI thread, and after it is completed, and when the UI thread is freed from its current tasks, it will execute a ProcessResult with data obtained from the second thread.
Is there some kind of magic? In fact, of course, there are a couple of minuses (which, by the way, we will eliminate in our implementation), but in general it looks like exactly what we need for the convenience of writing asynchronous code. How does this magic work? In fact, it is very simple - the so-called coroutine co-procedures are used here.
Procedures
A simple procedure is a block of code with multiple entry points. They are used most often for cases of a very large number of parallel tasks (for example, in a server implementation), where the presence of a similar number of threads is completely inefficient. In this case, they allow you to create the appearance of threads (cooperative multitasking) and this greatly simplifies the code. Also with the help of coprocedure, you can implement the so-called generators. The implementation of co-procedures can be either built into the language, or as a library, and even provided by the OS (in Windows, co-procedures are called Fiber).
In C #, co-procedures were used not for such classical purposes, but for the implementation of curious syntactic sugar. The implementation here is built into the language, but far from the best. This is the so-called stackless implementation, which essentially is a finite state machine that stores the necessary local variables and entry points. It is from this that most of the drawbacks of the C # implementation result. And the need to arrange "async" throughout the call stack and the extra overhead of the machine. By the way, await is not the first appearance of coprocedures in C #. yield is the same thing, only more limited.
What about C ++? There is no co-procedure in the language itself, but there are many different implementations in the form of libraries. There is it in Boost as well, and there the most effective variant is implemented - stackfull. It works through saving / restoring all the registers of the processor and the stack, respectively - in fact, like real threads, only this is all without accessing the OS, so it is almost instantaneous. And like everything in Boost, it works fine on different operating systems, compilers, processors.
Well, since in C ++ we have an even more powerful implementation of co-procedures than in C #, it’s just a sin not to write our own version of await / async syntactic sugar.
C ++ implementation
Let's see what the Boost.Coroutine library gives us. First, we need to create an instance of the coroutine class, passing it our constructor (functor, lambda function) to the constructor, and this function should have one (maybe more, already for our purposes) parameter to which the special functor will be passed.
using Coro=boost::coroutines::coroutine<void()>; Coro c([](Coro::caller_type& yield){ ... yield();
The execution of our function begins immediately in the constructor of the co-procedure, but it continues only until the first call to the yield functor. Then immediately comes back from the constructor. Further, we can call our co-procedure at any time (which is also a functor) and execution will continue inside our function in the same context as it terminated after the yield call. Does this description not exactly correspond to the one required for the realization of the syntactic sugar we need?
Now we have everything we need. It remains to apply a bit of the magic of templates and macros (this is only to make it look quite like the C # option) and we get
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)
All implementation occupies some miserable 20 lines of the elementary code! Of course, you can put them in a separate hpp file and call them something like a library, but it will be ridiculous. True, we need to define a couple more lines already dependent on the choice of our GUI framework (or native api in general). Something like:
void Post2UI(const void* coro) {PostMessage(coro);} void OnAsync(Event& event) {CallFromUI(event.Get<void*>());}
But this is just a couple of lines, one for the whole application and the same for all applications on the same framework. After that we can easily write such code:
void Handler(Params params) async_code ( auto r = await_async([&]{return CalcSomething(params);}); ProcessResult(r); )
And the sequence of calculations will be exactly the same as in the C # variant. And we did not have to change the function signature (add async throughout the call stack) as in C #. Moreover, here we are not limited to running one asynchronous task for functions. We can start several asynchronous blocks for parallel execution at once, or even go through a loop in general. For example, this code:
void Handler(const list<string>& urls) { for(auto url: urls) async_code ( result+=await_async([&]{return CheckServer(url);}); ) }
runs a parallel CheckServer for each item in the list and collects all the results in the result variable. And it is obvious that no synchronization, locks and other things are required, since result + = ... code will be executed only in the UI stream. In C #, this is naturally also recorded without problems, but you have to do a separate function, which you call in a loop.
Testing
Despite the size and simplicity of our implementation, we will still test it to make sure that it works correctly. To do this, it is best to write on your favorite GUI framework the simplest test application from one input field (multi-line) and one button. Then our test will be generalized (removed extra details) look like this:
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 is worth checking for working with modal windows. The result:
Run asynchronous from stream 1
Run asynchronous from stream 1
Showing MessageBox from Stream 1
Showing result in stream 1: Work in stream 2 completed on data 12345
Displaying result in stream 1: Work completed in stream 3 on data 67890
MessageBox is closed on stream 1
Results
I think that now it is not necessary to explain the remark at the beginning of the article about libraries. With modern tools (C ++ 11, Boost), any C ++ programmer is able to write a full await / async implementation from C # in a few minutes and a dozen lines of code. Moreover, this implementation will also be more flexible (several async blocks per function), more convenient (no need to multiply async by the call stack) and much more efficiently (in the sense of overhead).
Literature
1.
en.cppreference.com/w/cpp/thread - support for multithreading in the standard library.
2.
www.boost.org/doc/libs/1_54_0/libs/coroutine/doc/html/index.html - implementation of
co -procedures in Boost.
Supplement 1
The comments rightly noted that await / async from C # can work not only with different threads, but also within one. Well, actually it is obviously the way it should be, because this is just not the best implementation of coprocedural procedures, and they were originally created just for this kind of work. And it’s natural that with the help of a co-procedure from Boost, this is implemented quite trivially. I did not show such a code just because it was not part of my initial formulation of the problem (see the beginning of the article), and I don’t see much sense in this code at all. But once in the comments they became interested in the community (to show that such a solution completely replaces await / async), I’ll show you here.
And so, we add to our implementation (which is of 20 lines) a few more:
template<typename L> auto __await(const __Coro* coro, __Coro::caller_type& yield, L lambda)->decltype(lambda()) { Post2UI(coro); yield(); return lambda(); } #define await(l) __await_async(__coro, __yield, l)
And after that we will be able to write like this (the code is exactly the same as before, only replaced await_async with await):
void Handler(Params params) async_code ( auto r = await([&]{return CalcSomething(params);}); ProcessResult(r); )
Here again, there will be a return of control from Handler immediately after calling await, but the remaining code will be executed in the same UI thread as Handler was executed, only sometime later, when it is free. No additional threads are naturally created here.