Async, Promises and Futures

Passing Data

In a lambda or variadic template thread, we pass data (via arguments or capture) from the parent thread (i.e. main) to the worker thread.

#include <iostream>
#include <thread>

void call_from_thread(int tid) {
  std::cout << "Thread #" << tid << ", id: " << std::this_thread::get_id() << std::endl;
}

int main() {
  std::cout << "Hello concurrent world, main Thread id : " << std::this_thread::get_id() << std::endl;
  // async launch a thread

  int tid = 1;
  std::thread t1(call_from_thread, tid);

  tid = 2;
  std::thread t2([tid] {
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::cout << "Thread #" << tid << ", id: " << std::this_thread::get_id() << std::endl;
  }); 

  // thread barrier 
  t1.join(); 
  t2.join();

  return 0;
}

Output:

➜  g++ -pthread -std=c++11 multi_threads-barrier\ copy.cpp 
➜ ./a.out                                                 
Hello concurrent world, main Thread id : 0x11c94fe00
Thread #1, id: 0x7000043e1000
Thread #2, id: 0x700004464000

passing data from child to parent

In the C++ standard, to pass data in the opposite direction, the threads must be syncronized (via a syncronization protocol). Using a single-use channel between threads, we can pass data between the two, the sending end of the channel is called Promise, the receiving end Future.

exception example

To pass data from a child thread to a parent thread (main), a thread function can store a value or an exception through a promise.

Example of storing a thrown error in the promise.

#include <thread>
#include <iostream>
#include <future>
 
int main()
{
    std::promise<int> prms;
    std::future<int> ftr = prms.get_future();
 
    std::thread t([&prms]{
        try {
            // code that may throw
            // prms.set_value(42);
            throw std::runtime_error("Example");
        } catch(...) {
            prms.set_exception(std::current_exception());
        }
    });
 
    try {
        std::cout << "Value: " << ftr.get();
    } catch(const std::exception& e) {
        std::cout << "Exception: " << e.what() << '\n';
    }
    t.join();

    return 0;
}

Promise

A promise creates a Channel.

  • std::promise::set_value() to set the return value and makes the state ready.
  • std::promise::set_value_at_thread_exit(e) all from above, but sets at the end of the thread.
  • std::promise::set_exception() to set a exception and makes the state ready.
  • std::promise::set_exception_at_thread_exit(e) all from above, but sets at the end of the thread.

std::promise

Future

As in life, a promise is useless without Future. Future is used to retrieve the promised value.

A std::future connects to the channel and waits to read and return the data once it is written. To pair a Promise with a Future promise::get_future.

if a std::future::valid() returns true: std::future::wait() to perform a blocking wait until the result becomes available (or std::future::wait_for(timeout)) and std::future::get() to return the provided result or an exception.

⚠️ std::future::get() invalidates the state, so “get” can be called only once

Syncronization: a future get call blocks (task), a thread join is waiting (thread).

ℹ️ A std::shared_future is copyable or can refer to shared state (with other asycronous return objects). std::shared_future

std::future

Async

Passing data from a worker to the parent thread involves some overhead and boilerplate code (rvalue reference and std::move). A simpler (or higher level) option is to use std::async instead of std::thread.

std::async returns a std::future.

#include <iostream>
#include <thread>
#include <future>
#include <cmath>
#include <memory>

unsigned int square(int num)
{
    std::this_thread::sleep_for(std::chrono::milliseconds(500)); 

    if (num < 0)
        throw std::runtime_error("Exception from thread: num must greater or equals than zero");

    return num * num;
}


int main()
{
    auto num = -42; 
    
    // num = 3; 

    std::future<unsigned int> ftr = std::async(square, num);

    try {
        auto result = ftr.get();
        std::cout << "Result = " << result << std::endl;
    }
    catch (std::runtime_error e) {
        std::cout << e.what() << std::endl;
    }

    return 0;
}

A simple example where async takes a callable (lambda) and stores the result in a vector of futures.

#include <iostream>
#include <vector>
#include <future>


int main()
{
    std::vector<std::future<int>> futures;
    
    for(int i = 0; i < 10; ++i) {
      futures.push_back (std::async([](int num) {return num * num;} , i));
    }

    // retrieve results 
    for(auto &e : futures) {
      std::cout << e.get() << std::endl;
    }

    return 0;
}

It is also possible to make “fire and forget” calls over std::launch::async policy, which enforces the OS to create a thread.

C++ Async

Written on April 29, 2021
[ c++  ]