home page -> teaching -> parallel and distributed programming -> Lecture 5 - Asynchronous functions and futures with continuations

Lecture 5 - Asynchronous functions and futures with continuations

This is an easier mechanism to work with, compared to the condition variables. Essentially, we have an object that exposes two interfaces:

Examples:

futures-demo1.cpp
using futures to get the result from asynchronous tasks
futures-demo1-with-impl.cpp
as above, but with a possible implementation for futures and the async() call
futures-demo1.cs
as above, but in C#
futures-demo2-cascade1.cs
cascading tasks through the ContinueWith() mechanism
futures-demo2-cascade2.cs
same as above, but showing that the actual execution is de-coupled from the setup
futures-demo3-when-all.cs
using the WhenAll() mechanism to start a task only after all its input data is computed (by other tasks)

Handling operations that depend on external events

Blocking calls

  recv(sd, data, len); // blocks until data is available
  ... // process
  send(sd, result, len);
  ...

Event-driven, select()

  while(true) {
      select(nr, readFds, writeFds, exceptFds, nullptr);
      if(FD_ISSET(sd, readFds) {
          recv(sd, data, len); // a single read is guaranteed not to block
          ...// process
      }
  }
  ...

Event-driven, based on callbacks:

There is a begin...() call that initiates the operation and sets a callback that will be executed on completion. The begin...() operation returns immediatey an identifier of the asynchronous operation. The callback is called by the library when the operation completes. The callback (or someother thread) needs to call the corresponding end...() operation, that returns the results of the operation and frees the associated resources in the library.

  class Receiver {
      void Callback(IAsyncResult ar) {
          int receivedBytes = sd.EndReceive(ar);
          // process data
          if(expectMoreData) {
              sd.BeginReceive(m_buf, m_offset, m_bufSize, 0, Callback, null);
          }
      }
      
      void Start() {
          // ...
          sd.BeginReceive(m_buf, m_offset, m_bufSize, 0, Callback, null);
      }
  }
  ...

A complete server implementation (for a very simple server) is given in srv-begin-end.cs.

Features:

Combining callbacks with futures and continuations

The idea is that the begin...() call is inside a function that returns a future, and the callback completes that future.

The previous server re-implemented using futures: srv-task.cs. In this phase, the continuations are used as a way to trigger callbacks, like in the callbacks based implementation.

To have a nicer implementation, we need a mechanism to to better compose the asynchronous operations. Especially important is a mechanism to create a loop executing an asynchronous operation.

The previous server re-implemented again using the loop mechanism and composing asynchronous operations is srv-tasks-loop.cs.

As a note, an experimental framework for C++ for composing asynchronous operations is available at https://github.com/rlupsa/futures-demo. A more developed framework, including both futures with continuations and C++20 coroutines is available at https://github.com/rlupsa/carpal.

The async-await mechanism

The previous server re-implemented using async-await: srv-await.cs.

Radu-Lucian LUPŞA
2023-11-05