std::promise and std::future (And Some std::chrono) (Some Live Hacking)

Overview

#include <future>

One-shot communication device

  • One thread promises to produce a value

  • Another thread waits for it to become valid in the future

std::promise<int> promise;
auto future = promise.get_future();

// producer ...
promise.set_value(42);

// consumer ...
int value = future.get();

(See below for how to transport exceptions across thread boundaries)

Overview: std::promise

Operation

Description

Constructor

  • Default

  • Move

operator=()

Move only

get_future()

Get std::future object connected to this promise

set_value()

Make promise come true (i.e. consumer will return from future.get())

set_exception(std::exception_ptr p)

Consumer’s future.get() will throw exception pointed to by p

Note

Communication is only one-shot: cannot set value (or exception) multiple times

Overview: std::future

Operation

Description

Constructor

  • Default. Object is not valid; use promise.get_future() instead

  • Move

operator=()

Move only

share()

Create a std::shared_future object. The original future object is invalid afterwards.

get()

If value is available, return it. Else, wait and then return.

wait()

Wait for value to become available, without getting it.

wait_for(), wait_until()

Timeouts of various sorts

Utterly Wrong: Waiting For Something To Become Ready

  • Separate, uncoordinated, flag and answer

  • Use nanosleep() to poll/wait

#include <thread>
#include <iostream>
#include <cassert>
#include <time.h>


static const unsigned TEN_MILLION_YEARS_S = 2;
static const unsigned ANSWER_POLL_INTERVAL_MS = 2;

int main()
{
    int answer;
    bool answer_valid;

    std::thread chew_answer([&answer, &answer_valid]() {
        // chew on world until we know the answer
        timespec ts { TEN_MILLION_YEARS_S, 0 };
        int error = nanosleep(&ts, nullptr);
        assert(!error);

        answer = 42;
        answer_valid = true;
    });
    
    while (! answer_valid) {
        timespec ts { 0, ANSWER_POLL_INTERVAL_MS * 1000*1000 };  // assuming less than a second
        int error = nanosleep(&ts, nullptr);
        assert(!error);
    }
    std::cout << answer << std::endl;

    chew_answer.join();
    return 0;
}
  • Bugs

    • Unlocked

    • CPU may reorder ⟶ flag may be in place before answer is

  • Polling interval

    • Less latency ⟶ more CPU time needed

    • Tight loop for maximum reaction

Sideways: std::chrono, And Literals

  • Replace <time.h> with <chrono>

  • Use cool literals with built-in units, and constexpr

    #include <chrono>
    
    using namespace std::literals::chrono_literals;
    
    static constexpr auto ANSWER_COMPUTATION_TIME = 2s;
    static constexpr auto ANSWER_POLL_INTERVAL = 2ms;
    
#include <thread>
#include <iostream>
#include <chrono>

using namespace std::literals::chrono_literals;

static const auto TEN_MILLION_YEARS = 2s;
static const auto ANSWER_POLL_INTERVAL = 2ms;

int main()
{
    int answer;
    bool answer_valid;

    std::thread chew_answer([&answer, &answer_valid]() {
        // chew on world until we know the answer
        std::this_thread::sleep_for(TEN_MILLION_YEARS);

        answer = 42;
        answer_valid = true;
    });
    
    while (! answer_valid)
        std::this_thread::sleep_for(ANSWER_POLL_INTERVAL);
    std::cout << answer << std::endl;

    chew_answer.join();
    return 0;
}

Minimal Fix: Use std::mutex

  • Add std::mutex to flag and answer

  • Don’t (yet) use scoped locking; rather use clumsy (non-exception-safe) lock() and unlock(), together with a done flag

#include <thread>
#include <mutex>
#include <iostream>
#include <chrono>

using namespace std::literals::chrono_literals;

static const auto TEN_MILLION_YEARS = 2s;
static const auto ANSWER_POLL_INTERVAL = 2ms;

int main()
{
    std::mutex lock;
    int answer;
    bool answer_valid;

    std::thread chew_answer([&answer, &answer_valid, &lock]() {
        // chew on world until we know the answer
        std::this_thread::sleep_for(TEN_MILLION_YEARS);

        lock.lock();
        answer = 42;
        answer_valid = true;
        lock.unlock();
    });

    bool done = false;
    while (! done) {
        lock.lock();
        if (answer_valid) {
            done = true;
            std::cout << answer << std::endl;
        }
        else
            std::this_thread::sleep_for(ANSWER_POLL_INTERVAL);
        lock.unlock();
    }

    chew_answer.join();
    return 0;
}

Anti-Clumsiness: Scoped Locking

  • Use std::scoped_lock ⟶ remove done flag, and simply break out of loop

#include <thread>
#include <mutex>
#include <iostream>
#include <chrono>

using namespace std::literals::chrono_literals;

static const auto TEN_MILLION_YEARS = 2s;
static const auto ANSWER_POLL_INTERVAL = 2ms;

int main()
{
    std::mutex lock;
    int answer;
    bool answer_valid;

    std::thread chew_answer([&answer, &answer_valid, &lock]() {
        // chew on world until we know the answer
        std::this_thread::sleep_for(TEN_MILLION_YEARS);

        lock.lock();
        answer = 42;
        answer_valid = true;
        lock.unlock();
    });

    while (true) {
        std::scoped_lock guard(lock);
        if (answer_valid) {
            std::cout << answer << std::endl;
            break;
        }
    }

    chew_answer.join();
    return 0;
}

Pro-Readability: Encapsulate

  • Create class Answer

  • Methods: set(), wait(duration)

#include <thread>
#include <mutex>
#include <iostream>
#include <chrono>

using namespace std::literals::chrono_literals;

static const auto TEN_MILLION_YEARS = 2s;
static const auto ANSWER_POLL_INTERVAL = 2ms;


class Answer
{
public:
    Answer() : _answer_valid(false) {}
    
    void set(int answer)
    {
        std::scoped_lock guard(_lock);
        _answer = answer;
        _answer_valid = true;
    }

    template <typename dur>
    int wait(dur d)
    {
        while (true) {
            std::scoped_lock guard(_lock);
            if (_answer_valid)
                return _answer;
            std::this_thread::sleep_for(d);
        }
    }

private:
    std::mutex _lock;
    int _answer;
    bool _answer_valid;
};

int main()
{
    Answer answer;

    std::thread chew_answer([&answer]() {
        // chew on world until we know the answer
        std::this_thread::sleep_for(TEN_MILLION_YEARS);
        answer.set(42);
    });

    std::cout << answer.wait(ANSWER_POLL_INTERVAL) << std::endl;

    chew_answer.join();
    return 0;
}

Atomics On Structures?

  • Nested struct data: flag and answer

  • Use std::atomic<data> inside class Answer

#include <thread>
#include <atomic>
#include <iostream>
#include <chrono>

using namespace std::literals::chrono_literals;

static const auto TEN_MILLION_YEARS = 2s;
static const auto ANSWER_POLL_INTERVAL = 2ms;


class Answer
{
public:
    void set(int answer)
    {
        _data = data(answer);
    }

    template <typename dur>
    int wait(dur d)
    {
        while (true) {
            data data = _data;
            if (data.answer_valid)
                return data.answer;
            std::this_thread::sleep_for(d);
        }
    }

private:
    struct data
    {
        data() noexcept : answer_valid(false), answer(0) {}
        data(int answer) noexcept : answer_valid(true), answer(answer) {}
        bool answer_valid;
        int answer;
    };

    std::atomic<data> _data;
};

int main()
{
    Answer answer;

    std::thread chew_answer([&answer]() {
        // chew on world until we know the answer
        std::this_thread::sleep_for(TEN_MILLION_YEARS);
        answer.set(42);
    });

    std::cout << answer.wait(ANSWER_POLL_INTERVAL) << std::endl;

    chew_answer.join();
    return 0;
}

Anti-Polling: Semaphore

  • No std::mutexstd::binary_semaphore (initialized with 0)

  • Discuss barrier instruction (signal/wait semaphore)

  • Discuss: _answer_valid not needed when properly waited

  • Discuss: OS wait conditions, blocking system calls, realtime

#if __cplusplus >= 202001L

#include <thread>
#include <semaphore>
#include <iostream>
#include <cassert>
#include <chrono>

using namespace std::literals::chrono_literals;

static constexpr auto TEN_MILLION_YEARS = 2s;


class Answer
{
public:
    Answer() : _notifier{0}
    {}
    
    void set(int answer)
    {
        _answer = answer;
        _notifier.release();
    }

    int wait()
    {
        _notifier.acquire();
        return _answer;
    }

private:
    std::binary_semaphore _notifier;
    int _answer;
};

int main()
{
    Answer answer;

    std::thread chew_answer([&answer]() {
        // chew on world until we know the answer
        std::this_thread::sleep_for(TEN_MILLION_YEARS);
        answer.set(42);
    });

    std::cout << answer.wait() << std::endl;

    chew_answer.join();
    return 0;
}

#else

#include <iostream>
using namespace std;

int main()
{
    cerr << "no!" << endl;
    return 0;
}

#endif

Getting To The Point: std::promise And std::future

#include <thread>
#include <future>
#include <iostream>
#include <chrono>

using namespace std::literals::chrono_literals;

static const auto TEN_MILLION_YEARS = 2s;


int main()
{
    std::promise<int> answer_promise;
    auto answer_future = answer_promise.get_future();

    std::thread chew_answer([&answer_promise]() {
        // chew on world until we know the answer
        std::this_thread::sleep_for(TEN_MILLION_YEARS);
        answer_promise.set_value(42);
    });

    std::cout << answer_future.get() << std::endl;

    chew_answer.join();
    return 0;
}

And Exceptions?

#include <thread>
#include <future>
#include <iostream>
#include <chrono>
#include <exception>

using namespace std::literals::chrono_literals;

static const auto TEN_MILLION_YEARS = 2s;


int main()
{
    std::promise<int> answer_promise;
    auto answer_future = answer_promise.get_future();

    std::thread chew_answer([&answer_promise]() {
        // chew on world until we know the answer
        std::this_thread::sleep_for(TEN_MILLION_YEARS);
        answer_promise.set_exception(std::make_exception_ptr(std::runtime_error("bummer!")));
    });

    try {
        answer_future.get();
    }
    catch (const std::runtime_error& e) {
        std::cerr << e.what() << std::endl;
    }

    chew_answer.join();
    return 0;
}