Hi everyone
I would love to see resumable functions/coroutines introduced into C++ standard. It would make my life, and lives of many other software developers, easier. It would solve many problems, encountered in many kinds of projects.
Unfortunately, I find the resumable/await proposal (N3858) flawed. I’ll describe the flaws below, and in another message I will present an alternative idea.
Problems with the resumable functions/await:
1. It does not solve real-life problems
As a programmer, I would like to use resumable functions to:
a. Implement generators,
b. Convert my multi-threaded, IO-heavy application into a single-threaded one, preserving the logical flow of functions, but using asynchronous IO,
c. Re-write my single-threaded applications using asynchronous IO (for instance boost.asio) to use coroutines, so that the performance gain from async io is preserved, while the code is cleaner, easier to read and maintain.
resumable/await would not help me with any of it. Writing generators is awkward (even authors of the proposal admit it chapter 5.2). As for asynchronous IO: there is no concrete example how to integrate resumable/await with any existing async IO solutions; instead, the proposal vaguely mentions a “scheduling logic inside the runtime”.
2. It’s high level solution
Resumable functions are high-level mechanisms implemented as language features. All the features provided by the solution could be implemented as a library instead, given the appropriate low-level primitives are available.
3. ‘await’ can only be used in resumable function.
The ‘await’ operation can only be used at the level of resumable function. In other words: the decision to suspend execution has to be taken in the function’s body, it can’t be delegated to another function.
I realise that this is by design, to allow for rewriting of the function body by the compiler, but this makes integration with existing code impossible.
Consider the following code:
void handle_http_request(std::istream& input, std::ostream& output)
{
Poco::Net::HTTPRequest request;
request.read(input);
// ... read request body here
// ... produce and write response here
}
This code could be either synchronous (using blocking IO) or asynchronous (using coroutines). The iostreams provide sufficient abstraction.
In the asynchronous case, the decision to suspend execution can only be made several levels below: below Poco’s HTTP, below std::iostream, below std::streambuf, at the point when the actual IO operation is performed.
4. std::future is required to suspend execution
future/promise may be nice, high-level idioms for asynchronous operations, but making them a requirement makes some use cases awkward or even impossible to implement. Generators are difficult to implement, and existing code would have to be wrapped or re-written.
5. Resumable functions are ‘special’
They have to be decorated, they have certain restrictions put on them, exceptions use is limited. Normal functions, functors or lambdas can’t be used as a resumable functions, just as they can be used as thread routines.
6. Many questions remain unanswered
We, C++ programmers like to know what’s going on under the covers. And the resumable functions proposal leaves too many questions unanswered:
a. What is the lifetime of objects created on stack in resumable functions?
b. When are the resources required to create such a function (side stack) released?
c. How are these functions resumed?
d. If get() is called on a future returned by resumable function, how will it block? What device will be used to block and how can it be controlled from the outside?
Unfortunately, I find the resumable/await proposal (N3858) flawed. I’ll describe the flaws below, and in another message I will present an alternative idea.
int bar(int i)
{
// await is not limited by "one level" as in C#
auto result = await async([i]{ return reschedule(), i*100; });
return result + i*10;
}
int foo(int i)
{
cout << i << ":\tbegin" << endl;
cout << await async([i]{ return reschedule(), i*10; }) << ":\tbody" << endl;
cout << bar(i) << ":\tend" << endl;
return i*1000;
}
void async_user_handler()
{
vector<future<int>> fs;
// instead of `async` at function signature, `asynchronous` should be
// used at the call place:
for(auto i=0; i!=5; ++i)
fs.push_back( asynchronous([i]{ return foo(i+1); }) );
for(auto &&f : fs)
cout << await f << ":\tafter end" << endl;
}