Skip to content

AFuture#

Represents a value that will be available at some point in the future.

Header:#include <AUI/Thread/AFuture.h>
CMake:aui_link(my_target PUBLIC aui::core)

Detailed Description#

Experimental Feature

This API is experimental. Experimental APIs are likely to contain bugs, might be changed or removed in the future.

AFuture is used as a result for asynchronous functions.

AFuture is returned by AUI_THREADPOOL keyword, which is used to perform heavy operations in a background thread.

AFuture<int> theFuture = AUI_THREADPOOL {
  AThread::sleep(1000); // long operation
  return 123;
};
...
cout << *theFuture; // waits for the task, outputs 123

If your operation consists of complex future sequences, you have multiple options:

  1. Use stackful coroutines. That is, you can use operator* and get() methods (blocking value acquiring) within a threadpool thread (including the one that runs AUI_THREADPOOL 's body). If value is not currently available, these methods temporarily return the thread to threadpool, effeciently allowing it to execute other tasks.

    Be aware fot std::unique_lock and similar RAII-based lock functions when performing blocking value acquiring operation.

  2. Use stackless coroutines. C++20 introduced coroutines language feature. That is, you can use co_await operator to AFuture value:

    AFuture<int> longOperation();
    AFuture<int> myFunction() {
      int resultOfLongOperation = co_await longOperation();
      return resultOfLongOperation + 1;
    }
    

  3. Use AComplexFutureOperation. This class creates AFuture (root AFuture) and forwards all exceptions to the root AFuture. This method is not recommended for trivial usecases, as it requires you to extensivly youse onSuccess method in order to get and process AFuture result, leading your code to hardly maintainable spaghetti.

For rare cases, you can default-construct AFuture and the result can be supplied manually with the supplyValue() method:

AFuture<int> theFuture;
AThread t([=] {
  AThread::sleep(1000); // long operation
  theFuture.supplyValue(123);
});
t.start();
cout << *theFuture; // 123

Be aware of exceptions or control flow keywords! If you don't pass the result, AFuture will always stay unavailable, thus all waiting code will wait indefinitely long, leading to resource leaks (CPU and memory). Consider using one of suggested methods of usage instead.

AFuture provides a set of functions for both "value emitting" side: supplyValue(), supplyException(), and "value receiving" side: operator->(), operator*(), get().

When AFuture's operation is completed it calls either onSuccess() or onError(). These callbacks are excepted to be called in any case. Use onFinally() to handle both.

AFuture is a shared_ptr-based wrapper so it can be easily copied, pointing to the same task.

If all AFutures of the task are destroyed, the task is cancelled. If the task is executing when cancel() is called, AFuture waits for the task, however, task's thread is still requested for interrupt. It guarantees that your task cannot be executed or be executing when AFuture destroyed and allows to efficiently utilize c++'s RAII feature.

To manage multiple AFutures, use AAsyncHolder or AFutureSet classes.

AFuture implements work-stealing algorithm to prevent deadlocks and optimizaze thread usage: when waiting for result, AFuture may execute the task (if not default-constructed) on the caller thread instead of waiting. See AFuture::wait for details.

Examples#

examples/app/game_of_life/src/main.cpp

Game of Life - Game of Life implementation that uses advanced large dynamic data rendering techniques such as ITexture, AImage to be GPU friendly. The computation is performed in AThreadPool.

    emits<> frameComplete;

private:
    _<ATimer> mTimer = _new<ATimer>(100ms);
    AFuture<> mFrame;
    glm::ivec2 mSize {};
    AVector<CellState> mStorage;
    AVector<CellState> mNextPopulation;

    CellState& get(AVector<CellState>& storage, glm::ivec2 position) {

Public Methods#

map#


template<aui::invocable<const T &> Callback >
auto AFuture::map(Callback&& callback)

Maps this AFuture to another type of AFuture.

onError#


template<aui::invocable<const AException &> Callback >
const AFuture& AFuture::onError(Callback&& callback)

Add onError callback to the future.

The callback will be called on the worker's thread when the async task is returned a result.

onError does not expand AFuture's lifespan, so when AFuture becomes invalid, onSuccess would not be called.

To expand lifespan, create an AAsyncHolder inside your window or object; then put the instance of AFuture there. Example:

...
private:
  AAsyncHolder mAsync;
...

mAsync << functionReturningFuture().onSuccess(...); // or onError

onFinally#


template<aui::invocable Callback >
const AFuture& AFuture::onFinally(Callback&& callback)

Adds the callback to both onSuccess and onResult.

supplyException#


void AFuture::supplyException(std::exception_ptr causedBy = std::current_exception())

Stores an exception from std::current_exception to the future.

supplyValue#


void AFuture::supplyValue(T v)

Pushes the result to AFuture.

Arguments
v
value

After AFuture grabbed the value, supplyValue calls onSuccess listeners with the new value.