Modern C++ concurrency mechanisms

Baosong Wu - Architect@SR-Intelligence

2020-08

Agenda

  • Fundamentals about std::thread 
  • Solutions to one-shot communication
    • Flag
    • Condition variable
    • Flag + Condition variable
    • void Future

Using std::thread

void foo()
{
    this_thread::sleep_for(chrono::seconds(1));
}

int main()
{
    thread t1(foo);

    thread t2([]() {
        foo();
    });
}

The destructor of std::thread

  • If std::thread is joinable, std::terminate() is called.
{
	std::thread t(funcToRun);	// t is joinable
	...				// assume t remians joinable
}					// std::terminate is called

std::thread::joinable()

  • A std::thread objet may be joinable
    • Represent an underlying thread of execution.
  • Unjoinable std:thread means no underlying TOE
    • Default constructed std::thread.
    • std::thread that has been detached or moved from.
    • std::thread whose TOE has been joined.
    • ...
void foo()
{
    this_thread::sleep_for(chrono::seconds(1));
}

int main()
{
    thread t1(foo);
 
    thread t2([]() {
        foo();
    });
 
    t1.join();
    t2.join();
}

Ensure std::thread is unjoinable when it is being destructed on all paths

  • Be unjoinable
    • Be joined
    • Or be detached
    • Or be moved from
    • Or ...
  • On all paths
    • continue, break, goto, return...
    • exception
class ThreadRAII
{
public:
    enum class DtorAction
    {
        Join,
        Detach
    };
    ThreadRAII(thread &&t, DtorAction action) : action(action), t(move(t)) {}
    ~ThreadRAII()
    {
        if (t.joinable())
        {
            if (action == DtorAction::Join)
            {
                t.join();
            }
            else
            {
                t.detach();
            }
        }
    }
    thread &get()
    {
        return t;
    }

private:
    DtorAction action;
    thread t;
};

Alternatives but not available

One-shot Communication

  • Scenario
    • Concurrent task dataProducer and dataProcessor.
    • In dataProducer, there is one event - data is ready.
    • dataProcessor can continue only if data is ready.
  • Problem
    • How dataProducer tells dataProcessor - the data is ready!

dataProducer

...

produce data

...

dataProcessor

...

Block until the data is ready.

...

std::atomic Flag

  • Basic idea:
    • dataProducer set the flag when data is ready.
    • dataProcessor keeps pulling the flag and proceeds only when the flag is set.

dataProducer

...

produce data

flag = true;

...

dataProcessor

...

while (!flag)

    ;

...

atomic<bool> flag(false);

std::atomic Flag

  • Problems:
    • Polling keeps dataProcessor running, even though its conceptually blocked.
      • Uses CPU.
      • Prevents other threads using its HW resource
      • Incurs context switch overhead each time it is scheduled

Condition Variables

Condition Variables 

  • Basic idea:
    • dataProcessor waits on a condvar.
    • dataProducer notifies the condvar when data is ready.

dataProducer

...

produce data

condVar.notify_one();

...

dataProcessor

...

unique_lock<mutex> lock(aMutex);

condVar.wait(lock);

...

mutex aMutex;

condition_variable condVar;

Condition Variables

  • Three problems:
    • Condvars require a mutex but there are state to protect.
      • 'Feels wrong'
    • Condvars may receive spurious notifications.
      • DataProcessor must verify that data is ready!!!
    • If dataProducor notifies before dataProcessor waits, dataProcessor blocks forever!!!
      • Must check to see if data is ready before waiting.

Condition Variables

Condition Variable + Flag

  • Basic idea:
    • dataProducer sets a flag and notifies condvar when data is ready
    • dataProcessor waits on a condvar, checks a flag when notified.

dataProducer

...
produce data
{
    unique_lock<mutex> lock(aMutex);
    flag = true;
}
condVar.notify_one();
...

dataProcessor

...

{
    unique_lock<mutex> lock(aMutex);

    condVar.wait(lock, [] { return flag; });
}

...

mutex aMutex;
condition_variable condVar;
bool flag = false;

Condition Variable + Flag

  • Problems:
    • It does work but seems unnecessarily complex
      • dataProducer set the flag - data is ready, but condvar notify still necessary.
      • dataProcessor wakes - data is probably ready but flag check still necessary.

void Future

promise<void> p;

thread t([&p] {
	p.get_future().wait();	// start t and suspend it
    funcToRun(); }
);

...				// t is waiting for p to be set
p.set_value();			// t may now continue
...
t.join();

void Future

dataProducer

...
produce data
p.set_value();

...

dataProcessor

...

p.get_future().wait();

...

promise<void> p;

void Future

  • What if dataProducer never managed to make data ready (e.g., due to an exception)
    • Flag: flag never set => dataProcessor pulls forever
    • Condvar: No notify => dataProcessor blocks forever (unless spurious wakes)
    • Condvar + flag: No notify + flag never set => dataProcessor blocks forever (unless spurious wakes)
  • Must use out-of-band mechanism to communicate 'data won't be ready'.

void Future

  • Observations:
    • Future can hold exceptions.
  • Solution:
    • Use shared promise
      • It's set value => dataProcessor 'receives' void.
      • Set exception if w/o being set => dataProcessor receives an exception.
    • In dataProcessor, use get instead of wait to allow it determine whether data won't be ready forever.

void Future

dataProducer

...

scope_guard(p.set_exception());
produce data
p.set_value();

scope_guard.Dismiss();

...

dataProcessor

...

try {

    p.get_future().get();

}

catch(...) {

    ...

}

...

promise<void> p;

void Future

  • void is content-free but it's availability isn't.
  • Unlike condvar:
    • No need for gratuitous mutex.
    • No need worry about "did the event already/really occur?"
  • Unlike std::atomic flag, no polling.

Recap

Modern C++ concurrency mechanisms

By bawu

Modern C++ concurrency mechanisms

  • 79