1245 | | Never block. Never call anything which blocks. |
| 1245 | This is going to be one of the hardest topics to write about given the highly uncertain present plans for resumable function support in C++ 1z. I base most of the following section on N4402 https://isocpp.org/files/papers/N4402.pdf and conversations with Gor Nishanov at C++ Now 2015, plus on conversations with Oliver Kowalke regarding proposed Boost.Fiber. |
| 1246 | |
| 1247 | Firstly, should you care about being C++ resumable function ready? If your library ever: |
| 1248 | |
| 1249 | * Uses threads. |
| 1250 | * Does i/o. |
| 1251 | * Uses callbacks, including `std::function`. |
| 1252 | |
| 1253 | Then the answer is yes! The current C++ 1z resumable function proposal provides two verbs to work with resumable functions: |
| 1254 | |
| 1255 | 1. `await`: suspend the execution of the current function, resuming when the callable passed to `await` finishes execution somewhere else or at some other time. Note that the name of this keyword may change in the future. Use of the `await` keyword always causes the invoking function to return some synchronisation object (often a `future<T>`) with a known `resumable_traits` specialisation, where the default catch all specialisation for any `R(T...)` callable is `std::future<R>`. |
| 1256 | |
| 1257 | This probably is a bit head wrecky, so let's look at some code: |
| 1258 | |
| 1259 | {{{#!c++ |
| 1260 | // This is some function which schedules some async operation whose result is returned via the future<int> |
| 1261 | extern std::future<int> async_call(); |
| 1262 | |
| 1263 | // This is a function which calls async_call ten times, suspending itself while async_call runs. |
| 1264 | std::future<int> accumulate() |
| 1265 | { |
| 1266 | int total=0; |
| 1267 | for(size_t n=0; n<10; n++) |
| 1268 | total+=await async_call(); |
| 1269 | return total; |
| 1270 | } |
| 1271 | |
| 1272 | int main(void) |
| 1273 | { |
| 1274 | std::cout << "Total is " << accumulate().get() << std::endl; |
| 1275 | return 0; |
| 1276 | } |
| 1277 | }}} |
| 1278 | |
| 1279 | The `await` keyword is rather like range for loops in that it expands into boilerplate. Let's look at the above code but with some of the boilerplate inserted, remembering that C++ 1z futures now have continuations support via the `.then(callable)` member function, and bearing in mind this is a simplified expansion of the boilerplate for the purposes of brevity: |
| 1280 | |
| 1281 | {{{#!c++ |
| 1282 | // This is some function which schedules some async operation whose result is returned via the future<int> |
| 1283 | extern std::future<int> async_call(); |
| 1284 | |
| 1285 | // This is a function which calls async_call ten times, suspending itself while async_call runs. |
| 1286 | std::future<int> accumulate() |
| 1287 | { |
| 1288 | // resumable_traits<R, Ts...> is specialised with the call signature of this function i.e. std::future<int>() |
| 1289 | // promise_type defaults to a wrap of type R::promise_type, so in this case a wrap of std::promise<int> |
| 1290 | // Note that constructing the promise wrap type does not necessarily construct the wrapped promise i.e. |
| 1291 | // actual construction may occur lazily |
| 1292 | auto __resume_promise(std::resumable_traits<std::future<int>>::promise_type()); |
| 1293 | if(__resume_promise.initial_suspend()) |
| 1294 | return suspend_now(); // Not expanded for brevity |
| 1295 | |
| 1296 | int total=0; |
| 1297 | for(size_t n=0; n<10; n++) |
| 1298 | { |
| 1299 | std::future<int> &&__temp=async_call(); // Bind to rvalue ref, no moving |
| 1300 | if(!__temp.await_ready()) // defaults to __temp.is_ready() |
| 1301 | { |
| 1302 | // The frame allocator defaults to std::allocator<char> |
| 1303 | auto &&__frame_allocator=std::resumable_traits<std::future<int>>::get_allocator(); |
| 1304 | |
| 1305 | // Internally generated by the compiler to resume me at the resume_me_detached goto label after storing my state into |
| 1306 | // memory allocated using __frame_allocator |
| 1307 | unspecified_callable &&__resume_me(__frame_allocator, __resume_promise, resume_me_detached); |
| 1308 | |
| 1309 | // defaults to __temp.then(__resume_me). __resume_me will be executed immediately after __temp.set_value() by the thread calling set_value(). |
| 1310 | __temp.await_suspend(__resume_me); |
| 1311 | |
| 1312 | // Return a future for the completion of this function. If the internal promise object is |
| 1313 | // lazily constructed, now is when it actually occurs |
| 1314 | return __resume_promise.get_return_object(); |
| 1315 | } |
| 1316 | never_resumed: // This path only taken if function not resumed |
| 1317 | total+=__temp.await_resume(); // defaults to __temp.get() |
| 1318 | } |
| 1319 | if(__resume_promise.final_suspend()) |
| 1320 | return suspend_now(); // Not expanded for brevity, jumps to resume_me_addr path |
| 1321 | return std::make_ready_future(total); // Cheap optimisation elidable future construction |
| 1322 | |
| 1323 | |
| 1324 | |
| 1325 | // *** ALTERNATIVE DETACHED CODE PATH FOR WHEN THE FUNCTION IS RESUMED *** |
| 1326 | for(size_t n=0; n<10; n++) |
| 1327 | { |
| 1328 | std::future<int> &&__temp=async_call(); // Bind to rvalue ref, no moving |
| 1329 | if(!__temp.await_ready()) // defaults to __temp.is_ready() |
| 1330 | { |
| 1331 | // defaults to __temp.then(__resume_me). __resume_me will be executed immediately after __temp.set_value() by the thread calling set_value(). |
| 1332 | __temp.await_suspend(__resume_me); |
| 1333 | |
| 1334 | // Suspend myself using previously allocated __resume_me |
| 1335 | __resume_me.suspend(); |
| 1336 | } |
| 1337 | resume_me_detached: // This path taken if function ever resumed |
| 1338 | total+=__temp.await_resume(); // defaults to __temp.get() |
| 1339 | } |
| 1340 | if(__resume_promise.final_suspend()) |
| 1341 | return suspend_now(); // Not expanded for brevity |
| 1342 | __resume_promise.set_result(total); // Signals the earlier returned future |
| 1343 | // No meaningful return from this function can occur |
| 1344 | } |
| 1345 | |
| 1346 | int main(void) |
| 1347 | { |
| 1348 | std::cout << "Total is " << accumulate().get() << std::endl; |
| 1349 | return 0; |
| 1350 | } |
| 1351 | }}} |
| 1352 | |
| 1353 | This boilerplate expanded version may still hurt the head, so here is essentially what happens: if `async_call()` ever returns an unsignaled future, `accumulate()` will construct a promise-future pair, allocate memory to store its stack frame, schedule the resumption of its detached self as a continuation of the future returned from `async_call()` and return the newly constructured future to `main()`. When the future returned by `async_call()` is signalled, this schedules the resumption of the detached version of `accumulate()` on the thread it was originally running on. This resumed `accumulate()` may suspend and resume itself many more times on the future returned by repeated calls of `async_call()`, but it eventually will signal the future it returned earlier with the result, unblocking `main()`. |
| 1354 | |
| 1355 | The `await_ready(), await_suspend()` and `await_resume()` functions are firstly looked up as member functions of the synchronisation type. If not present, they are then looked up as free functions within the same namespace as the synchronisation type with usual overload resolution. |
| 1356 | |
| 1357 | 2. `yield`: repeatedly suspends the execution of the current function with the value specified to yield, resuming higher up the call tree with that value output. Note that the name of this keyword may change in the future. Yield can be considered as more boilerplate sugar for a repeated construction of a promise-future pair then repeatedly signalled with some output value until the generating function (i.e. the function calling `yield`) returns, with the sequence of those repeated constructions and signals wrapped into an iterable such that the following just works: |
| 1358 | |
| 1359 | {{{#!c++ |
| 1360 | generator<int> fib(int n) |
| 1361 | { |
| 1362 | int a = 0; |
| 1363 | int b = 1; |
| 1364 | while (n-- > 0) |
| 1365 | { |
| 1366 | yield a; |
| 1367 | auto next = a + b; |
| 1368 | a = b; |
| 1369 | b = next; |
| 1370 | } |
| 1371 | } |
| 1372 | |
| 1373 | int main() |
| 1374 | { |
| 1375 | for (auto v : fib(35)) |
| 1376 | std::cout << v << std::endl; |
| 1377 | } |
| 1378 | }}} |
| 1379 | |
| 1380 | This just works because `generator<int>` provides an iterator at `generator<int>::iterator` and a promise type at `generator<int>::promise_type`. The iterator, when dereferenced, causes a single iteration of `fib()` between the `yield` statements and the value yielded is output by the iterator. |
| 1381 | |
| 1382 | TODO |
| 1383 | |