Continuations¶

  • Recap
    • Callbacks
      • Must be known in advance
      • Require functional form transformations
    • C++11 futures
      • Do not compose
      • Block on get()
  • Continuations combine the best features* of these two approaches for returning a value from a task
    • Do not need to be known in advance
    • Do not require functional transformations
    • Compose
    • Do not block
  • Continuations are part of futures in the concurrency TS in <experimental/future>
    • For this class I'm using stlab::future<> which has additional capabilities and minor differences in syntax
    • I'm participating in a standard committee workshop on April 26th to discuss the future of futures in C++20
  • A continuation is a callback attached to a future, using .then()
    • .then() returns a new future that can be used for more continuations
{
auto p = async(default_executor, []{ return 42; });

auto q = p.then([](int x){ cout << x << endl; });

auto r = q.then([]{ cout << "done!" << endl; });

blocking_get(r); // <-- DON'T DO THIS IN REAL CODE!!!
}
  • Recall our interned string implementation with futures
namespace bcc {

struct shared_pool {
    unordered_set<string> _pool;
    sequential_process _process;

    auto insert(string) -> stlab::future<const string*>;
};

auto shared_pool::insert(string a) -> stlab::future<const string*> {
    return async_packaged(_process, [this, _a = move(a)]() mutable {
        return &*_pool.insert(move(_a)).first;
    });
}

}
class interned_string {
    // struct shared_pool

    static auto pool() -> shared_pool& {
        static shared_pool result;
        return result;
    }

    shared_future<const std::string*> _string;
public:
    interned_string(string a) : _string(pool().insert(move(a))) {}

    auto str() const {
        return *_string.get(); // <---- BLOCKING!!!
    }
};
namespace {

class interned_string {
    // struct shared_pool

    static auto pool() -> shared_pool& {
        static shared_pool result;
        return result;
    }

    stlab::future<const string*> _string; // or std::experimental::shared_future
public:
    interned_string(string a) : _string(pool().insert(move(a))) {}

    auto str() const -> stlab::future<reference_wrapper<const string>> {
        return _string.then([](const string* p) { return cref(*p); });
    }
};

} // namespace
{
interned_string s("Hello World!"s);

auto done = s.str().then([](const string& s){
    cout << s << '\n';
});

blocking_get(done);
}
  • Pros for continuations
    • Do not need to be known in advance
    • Do not require functional transformations
      • Straight forward transformation from synchronous to asynchronous code
    • Compose
    • Do not block
  • Cons
    • Require more synchronization than callbacks
    • Execution context is ambiguous
      • Immediate execution may happen either in the calling context or resolving context
  • Multiple continuation can be attached to a single future (or shared_future)
    • This splits execution
    • i.e.
      • "blur the document"
        • "then save the document"
        • "then rotate the document"
  • A join is accomplished using when_all()
{
auto p = async(default_executor, []{ return 42; });
auto q = async(default_executor, []{ return 5; });

auto done = when_all(default_executor, [](int x, int y){ cout << x + y << endl; }, p, q);

blocking_get(done);
}
  • Joins are non-blocking
    • Attach continuations to the arguments
    • Keep an atomic count of how many arguments are resolved
    • Execute continuation on last resolve
  • Splits and Joins allow us to construct arbitrary dependency DAGs
  • Continuations are a form of a sequential process