Changes between Version 30 and Version 31 of BestPracticeHandbook


Ignore:
Timestamp:
May 26, 2015, 6:31:33 PM (7 years ago)
Author:
Niall Douglas
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • BestPracticeHandbook

    v30 v31  
    388388
    389389{{{#!c++
     390// handle_type is some class which takes ownership of a valid file descriptor, closing it on type destruction.
     391
    390392std::shared_ptr<handle_type> openfile(std::filesystem::path path)
    391393{
     
    12431245== 17. FUTURE PROOFING: Consider being C++ resumable function ready ==
    12441246
    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.
     1247This is going to be one of the hardest topics to write about given the highly uncertain present plans for resumable function/coroutine 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. Gor also kindly supplied me with a pre-draft N4499.
    12461248
    12471249Firstly, should you care about being C++ resumable function ready? If your library ever:
     
    12531255Then the answer is yes! The current C++ 1z resumable function proposal provides two verbs to work with resumable functions:
    12541256
    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>`.
     12571. `await`: ''potentially'' suspend the execution of the current function and return a future result now, resuming this function's execution 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>`.
    12561258
    12571259 This probably is a bit head wrecky, so let's look at some code:
     
    12661268  int total=0;
    12671269  for(size_t n=0; n<10; n++)
    1268     total+=await async_call();
     1270    total+=await async_call();  // Note the await keyword, this is where the function might suspend
    12691271  return total;
    12701272}
     
    12771279 }}}
    12781280
    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:
     1281 The `await` keyword is rather like range for loops in that it expands into a well specified 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 not actual expansion of the boilerplate for the purposes of brevity:
    12801282
    12811283 {{{#!c++
     
    12861288std::future<int> accumulate()
    12871289{
    1288   // resumable_traits<R, Ts...> is specialised with the call signature of this function i.e. std::future<int>()
     1290  // coroutine_traits<R, Ts...> is specialised with the call signature of this function i.e. std::future<int>()
    12891291  // 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()
     1292
     1293  // Always dynamically allocate a context on entry to a resumable function (the frame allocator defaults to std::allocator<char>)
     1294  auto &__frame_allocator=std::coroutine_traits<std::future<int>>::get_allocator();
     1295  struct __context_type
     1296  {
     1297    std::coroutine_traits<std::future<int>>::promise_type __resume_promise;
     1298    ... <args, stack, state> ...
     1299  } *__context=__frame_allocator.allocate(sizeof(__context_type));
     1300
     1301  // Construct the context frame
     1302  new (__context) __context_type(<args, stack, state>);
     1303
     1304  // Generate a future for the completion of this function
     1305  auto __return_object(__context->__resume_promise.get_return_object());
     1306 
     1307  if(__context->__resume_promise.initial_suspend())
     1308    return suspend_now();                          // Not expanded for brevity
     1309
     1310  try
     1311  {
     1312    int total=0;
     1313    for(size_t n=0; n<10; n++)
    13011314    {
    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      std::future<int> &&__temp=async_call();        // Bind to rvalue ref, no moving
     1316      if(!__temp.await_ready())                      // defaults to __temp.is_ready()
     1317      {
     1318        // Internally generated by the compiler to resume me at the resume_me_detached goto label
     1319        // after storing my state into __context
     1320        unspecified_callable &&__resume_me(__frame_allocator, __resume_promise, resume_me_detached);
     1321
     1322        // Have __temp, when signalled, resume my execution. defaults to __temp.then(__resume_me).
     1323        // __resume_me will be executed immediately after __temp.set_value() by the thread calling set_value().
     1324        __temp.await_suspend(__resume_me);
     1325
     1326        return __return_object;                      // exits function with future, will resume and signal later
     1327      }
     1328never_resumed:                                       // This path only taken if function never resumed
     1329      total+=__temp.await_resume();                  // defaults to __temp.get(), this is always ready and does not block
    13151330    }
    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())
     1331    __context->__resume_promise.set_value(total);
     1332  }
     1333  catch(...)
     1334  {
     1335    __context->__resume_promise.set_exception(std::current_exception());
     1336  }
     1337  if(__context->__resume_promise.final_suspend())
    13201338    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
     1339  __context->~__context_type();
     1340  __frame_allocator.deallocate(__context, sizeof(__context_type));
     1341  return __return_object;
    13221342 
    13231343
    13241344
    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()
     1345    // *** ALTERNATIVE DETACHED CODE PATH FOR WHEN THE FUNCTION IS RESUMED ***
     1346    for(size_t n=0; n<10; n++)
    13301347    {
    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();
     1348      std::future<int> &&__temp=async_call();        // Bind to rvalue ref, no moving
     1349      if(!__temp.await_ready())                      // defaults to __temp.is_ready()
     1350      {
     1351        // Have __temp, when signalled, resume my execution. defaults to __temp.then(__resume_me).
     1352        // __resume_me will be executed immediately after __temp.set_value() by the thread calling set_value().
     1353        __temp.await_suspend(__resume_me);
     1354
     1355        // Suspend myself using previously allocated __resume_me
     1356        __resume_me.suspend();
     1357      }
     1358resume_me_detached:                                  // This path taken if function ever resumed
     1359      total+=__temp.await_resume();                  // defaults to __temp.get(), this is always ready and does not block
    13361360    }
    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
     1361    __context->__resume_promise.set_value(total);
     1362  }
     1363  catch(...)
     1364  {
     1365    __context->__resume_promise.set_exception(std::current_exception());
     1366  }
     1367  if(__context->__resume_promise.final_suspend())
     1368    return suspend_now();                            // Not expanded for brevity, jumps to resume_me_addr path
     1369  __context->~__context_type();
     1370  __frame_allocator.deallocate(__context, sizeof(__context_type));
    13431371  // No meaningful return from this function can occur
    13441372}
     
    13511379 }}}
    13521380
    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.
     1381 This boilerplate expanded version may still hurt the head, so here is essentially what happens:
     1382 1. We dynamically allocate a stack frame on entry, and that dynamic memory allocation sticks around until the resumable function completes.
     1383 2. We always construct a promise-future pair for the result (or whatever synchronisation object `coroutine_traits` says) on entry to the function.
     1384 3. If `async_call()` always returns a signalled future, we add the result to total, signal the future and return the future.
     1385 4. If `async_call()` ever returns an unsignaled future, we ask the unsignaled future to resume our detached path when it is signalled. We suspend ourselves, and return our future immediately. At some point our detached path will signal the future.
     1386
     1387 The `await_ready(), await_suspend()` and `await_resume()` functions are firstly looked up as member functions of the synchronisation type returned by the traits specialisation. If not present, they are then looked up as free functions within the same namespace as the synchronisation type with usual overload resolution.
    13561388
    135713892. `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:
     
    13781410 }}}
    13791411
    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.
     1412 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. Note that for each iteration, a new promise and future pair is created, then destroyed and created, and so on until the generator returns.
     1413
     1414All this is great if you are on Microsoft's compiler, but what about the rest of us before C++ 1z? Luckily [http://olk.github.io/libs/fiber/doc/html/ Boost has a conditionally accepted library called Boost.Fiber] which was one of the C++ 11/14 libraries reviewed above and this, with a few caveats, provides good feature parity with proposed C++1z coroutines. Boost.Fiber provides a mirror image of the STL threading library, so:
     1415
     1416|| `std::thread`             || => || `fiber::fiber`              ||
     1417|| `std::this_thread`        || => || `fiber::this_fiber`         ||
     1418|| `std::mutex`              || => || `fiber::mutex`              ||
     1419|| `std::condition_variable` || => || `fiber::condition_variable` ||
     1420|| `std::future<T>`          || => || `fiber::future<T>`          ||
     1421
     1422Rewriting the above example to use Boost.Fiber instead (TODO):
     1423
     1424{{{#!c++
     1425// This is some function which schedules some async operation whose result is returned via the future<int>
     1426extern std::future<int> async_call();
     1427
     1428// This is a function which calls async_call ten times, suspending itself while async_call runs.
     1429std::future<int> accumulate()
     1430{
     1431  int total=0;
     1432  for(size_t n=0; n<10; n++)
     1433    total+=await async_call();  // Note the await keyword, this is where the function might suspend
     1434  return total;
     1435}
     1436
     1437int main(void)
     1438{
     1439  std::cout << "Total is " << accumulate().get() << std::endl;
     1440  return 0;
     1441}
     1442}}}
    13811443
    13821444TODO
    1383 
    13841445
    13851446Everything i/o async. Boost.Fiber.