Changes between Version 29 and Version 30 of BestPracticeHandbook


Ignore:
Timestamp:
May 22, 2015, 1:44:16 PM (7 years ago)
Author:
Niall Douglas
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • BestPracticeHandbook

    v29 v30  
    12431243== 17. FUTURE PROOFING: Consider being C++ resumable function ready ==
    12441244
    1245 Never block. Never call anything which blocks.
     1245This 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
     1247Firstly, 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
     1253Then the answer is yes! The current C++ 1z resumable function proposal provides two verbs to work with resumable functions:
     1254
     12551. `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>
     1261extern std::future<int> async_call();
     1262
     1263// This is a function which calls async_call ten times, suspending itself while async_call runs.
     1264std::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
     1272int 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>
     1283extern std::future<int> async_call();
     1284
     1285// This is a function which calls async_call ten times, suspending itself while async_call runs.
     1286std::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    }
     1316never_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    }
     1337resume_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
     1346int 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
     13572. `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++
     1360generator<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
     1373int 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
     1382TODO
     1383
    12461384
    12471385Everything i/o async. Boost.Fiber.