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 calledstd::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
- GSL::joining_thread
- CP.25: Prefer gsl::joining_thread over std::thread
- But the PR still not merged.
- std::jthread (C++20)
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
- Polling keeps dataProcessor running, even though its conceptually blocked.
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.
- Condvars require a mutex but there are state to protect.
Condition Variables
-
CP.42: Don't wait without a condition
- Reason: A wait without a condition can miss a wakeup or wake up simply to find that there is no work to do.
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.
- It does work but seems unnecessarily complex
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.
- Use shared promise
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
- We talked about:
- std::thread and RAII wrapper
- C++ concurrency mechanisms:
- std::atomic Flag
- std::condvar
- std::condvar + Flag
- void Future
- References:
- Code

Modern C++ concurrency mechanisms
By bawu
Modern C++ concurrency mechanisms
- 79