1
COSC 6374
Parallel Computation
Threading in C++ 11
Some slides based on material of Bo Qian
by http://boqian.weebly.com/c-programming.html
Edgar Gabriel
Fall 2015
C++ History
Image source: http://www.infoq.com/news/2014/08/cpp14-here-features
• Imperative, object-oriented, compiled programming
language
• Originally develop by Bjarne Stroustrup at Bell Labs (~1979)
• 1985: first commercial compiler
• 1989: C++ version 2.0 was relead
2
C++ 11 Threading
• Threading in C++ 11
– Operating System and Platform independent interfaces
– Low level interfaces ( i.e. mimicking pthreads)
• threads, mutexes, condition variables
– High level interfaces
• Easier utilization
• Integration with other language features of C++
• async, future, promise, packaged_tasks
Simple Example
#include <iostream>
#include <thread>
using namespace std;
void function_1( string msg) {
cout << “Hello World from thread " << msg << endl;
}
int main () {
thread t1(function_1, string(“1”)); // t1 starts running
t1. join(); // main thread waits for t1 to finish
return 0;
}
3
Thread management
• Starting a thread using the C++ threading library is equal to constructing a std::thread object
• Master thread waits on child thread in the join()
operations
• Master thread can choose to detach from thread if it doesn’t want to wait for it later using detach()
– C++ runtime library responsible for reclaiming resources
of thread
– Master thread can not rejoin the thread
• Functionality available to check whether joining a thread is possible, e.g.
if ( t1. joinable() ) t1.join()
Thread management
• Determine the id of a thread using get_id()
• Determine the number of recommended hardware
threads by using
std::thread::hardware_concurrency()
• std::thread instance owns a resource. Ownership
can be transferred between instances, but only through
move and but not copy semantics std::thread t1(f1);
std::thread t2 = t1; // doesn’t work!!
But
std::thread t2 = std::move(t1);
4
Dealing with exception
int main () {
std::thread t1(function_1, string(“1”));
// if main thread throws an exception, child thread
// would be terminated. Need to catch exceptions
// to avoid this
try {
for ( int i =0; i<100; i++ )
cout << t1.get_id() << "from main " << i << endl;
}
catch (...) {
t1.join();
throw;
}
t1. join(); // main thread waits for t1 to finish
ret;
}
Mutex lock
#include <mutex>
std::mutex mu;
void shared_print ( string msg, int id ){
mu.lock();
cout << msg << id << endl;
mu.unlock();
}
void function_1() {
shared_print (string("From t1: "),1);
}
int main () {
std::thread t1(function_1);
shared_print(string("From main: "),0);
t1. join();
return 0;
}
If cout throws an exception, mu never unlocked
5
Lock guard
• Locking and unlocking manually is error-prone, especially
when dealing with exceptions
• C++11 provides lock templates
– std::lock_guard does a simple lock and unlock
– std::unique_lock allows full control and multiple
lock/unlocks
• A lock guard is an object that manages a mutex object by
keeping it always locked.
– mutex object is locked by the calling thread upon
construction
– mutex is unlocked upon destruction
– guarantees the mutex is properly unlocked in case an
exception is thrown
Lock_guard
#include <mutex>
#include <thread>
std::mutex mu;
void shared_print ( string msg, int id ){
std:lock_guard<std::mutex> guard(mu);
cout << msg << id << endl;
}
void function_1() {
shared_print (string("From t1: "),1);
}
int main () {
std::thread t1(function_1);
shared_print(string("From main: "),0);
t1. join();
return 0;
}
6
Unique lock
• Unique lock is an object that manages a mutex
object with unique ownership in both states: locked
and unlocked.
• On construction the object acquires a mutex object,
for whose locking and unlocking operations becomes
responsible.
• Class guarantees an unlocked status on destruction
• Lock/unlock can be called arbitrarily often on a unique
lock, lifetime of the object is the upper bound
Unique lock
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mu;
void shared_print ( string msg, int id ){
std:unique_lock<std::mutex> ul(mu);
cout << msg << id << endl;
ul.unlock();
…
// do something else
}
7
Lazy initialization
• What if shared_print is never used? File has been
unnecessarily
• Solution: open file upon first usage
class LogFile{
std::mutex _mu;
ofstream _f;
public:
LogFile() {
_fopen(“log.txt”);
}
// destructor not shown here for closing the file
void shared_print( string id, int value) {
std::unique_lock<mutex>(_mu);
_f << “From “ << id << value << endl
_mu.unlock();
}
Lazy initialization
Syntactically correct, but introduces significant overhead since every call to shared_print will require two mutex locks!
class LogFile{
std::mutex _mu;
std::mutex _mu_open;
ofstream _f;
public:
LogFile() {}
// destructor not shown here for closing the file
void shared_print( string id, int value) {
std::uniqe_lock<mutex> locker2(_mu_open);
if ( !_f_is_open() ){
_f.open(“log.txt”);
locker2.unlock(;)
}
std::unique_lock<mutex> locker(_mu);
_f << “From “ << id << value << endl
_mu.unlock();
} }
8
Lazy initialization class LogFile{
std::mutex _mu;
std::once_flag _flag;
ofstream _f;
public:
LogFile() {
_fopen(“log.txt”);
}
// destructor not shown here closing the file
void shared_print( string id, int value) {
std::call_once(_flag, [&](){_f.open("log.txt");});
std::unique_lock<mutex>(_mu);
_f << “From “ << id << value << endl
_mu.unlock();
}
call_once(flag, fn, args)
• Calls fn passing args as arguments, unless another
thread has already executed (or is currently executing) a call to call_once with the same flag.
• Only one threads calls fn, all others do not call fn but
do not return until the fn itself has returned, and
all visible side effects are synchronized at that point
among all concurrent calls to this function with the
same flag.
• All future calls to call_once (with the same flag)
also return without executing
9
Consumer-producer example std::mutex mu;
std::deque<int> q;
void function_1() { // executing in thread 1
std::unique_lock<mutex> locker(mu, std::defer_lock);
while ( !done) { // Do something and add to q
locker.lock();
q.push_front(count);
locker.unlock();
}
}
void function_2() { // executed in thread 2
while ( !done ) {
std::unique_lock<mutex> locker(mu);
if ( !q.empty() ) {
data = q.back();
q.pop_back();
locker.unlock();
}
else { locker.unlock(); }
}
}
Busy wait: bad for performance
Condition variables
std::mutex mu;
std::deque<int> q;
std::condition_variable cond;
void function_1() { // executed in thread 1
std::unique_lock<mutex> locker(mu, std::defer_lock);
while ( !done ) {// Do something and add to q
locker.lock();
q.push_front(count);
locker.unlock();
cond.notify_one(); // wake up one thread
}
}
void function_2() { // executed in thread 2
while ( !done ) {
std::unique_lock<mutex> locker(mu);
cond.wait(locker);
data = q.back();
q.pop_back();
locker.unlock();
}
}
10
Condition variables std::mutex mu;
std::deque<int> q;
std::condition_variable cond;
void function_1() { // executed in thread 1
std::unique_lock<mutex> locker(mu, std::defer_lock);
while ( !done ) {// Do something and add to q
locker.lock();
q.push_front(count);
locker.unlock();
cond.notify_one(); // wake up one thread
}
}
void function_2() { // executed in thread 2
while ( !done ) {
std::unique_lock<mutex> locker(mu);
cond.wait(locker, [](){return !q.empty();});
data = q.back();
q.pop_back();
locker.unlock();
}
}
Spurious wakeup: a thread can wake up for other reasons than being notified
Returning values from threads
• How can we return the value of factorial from child thread
to the main thread?
void factorial( int N ) {
int res = 1;
for ( int i=N; i> 1; i-- )
res *=i;
cout << "Result is: "<< res << endl;
}
int main () {
int x;
std::thread t1(factorial, 4);
t1.join(); // main thread waits for t1 to finish
return 0;
}
11
Returning values from threads
• x now needs to be protected by a mutex
• Child thread needs to set value first -> condition variable
-> codes get complicated
void factorial( int N, int& x) {
int res = 1;
for ( int i=N; i> 1; i-- )
res *=i;
x=res;
}
int main () {
int x;
std::thread t1(factorial, 4, std::ref(x));
t1.join(); // main thread waits for t1 to finish
return 0;
}
async and futures #include <iostream>
#include <future>
using namespace std;
int factorial( int N ) {
int res = 1;
for ( int i=N; i> 1; i-- )
res *=i;
return res;
}
int main () {
int x;
std::future<int> fu = std::async(factorial, 4);
x = fu.get();
return 0;
}
12
Async
• async(fn,args): Calls fn (with args as arguments)
at some point, returning without waiting for the execution of fn to complete.
• The value returned by fn can be accessed through
the future object returned
• async may or may not create a new thread
std::launch::async => “as if” in a new thread.
std::launch::deferred => executed on demand.
std::launch::async | std::launch::deferred =>
implementation chooses (default).
async and futures #include <iostream>
#include <future>
using namespace std;
int factorial( int N ) {
int res = 1;
for ( int i=N; i> 1; i-- )
res *=i;
return res;
}
int main () {
int x;
std::future<int> fu = std::async(std::launch::async,
factorial, 4);
x = fu.get(); // get can be called only once
return 0;
}
13
future
• A future is an object that can retrieve a value from
some provider object or function, properly
synchronizing this access if in different threads.
• Calling future::get on a valid future blocks the
thread until the provider makes the shared state ready
(either by setting a value or an exception to it).
• future::get can only be called once on a future
• future has move semantics
• Question: can we use this mechanism to pass data from parent to child as well after launching the async
function?
– Yes. You need a promise
async, futures and promises #include <future>
using namespace std;
int factorial( std::future<int>& f ) {
int res = 1;
int N = f.get();
for ( int i=N; i> 1; i-- )
res *=i;
return res;
}
int main () {
std::promise<int> p;
std::future<int> f = p.get_future();
std::future<int> fu = async(factorial, std::ref(f));
// here we have the data
p.set_value(4);
int x = fu.get();
}
future has to be passed by reference, since it doesn’t support copy semantics
14
promise
• A promise is an object that can store a value to be
retrieved by a future object (possibly in another thread)
• On construction, promise objects are associated to a
new shared state on which they can store a value of type T
• This shared state can be associated to a future object by
calling member get_future(). After the call, both
objects share the same shared state: - The promise object is the asynchronous provider and is
expected to set a value for the shared state at some point. - The future object is an asynchronous return object that
can retrieve the value of the shared state, waiting for it to
be ready, if necessary.
Communicating between threads
void func1( std::promise<int> p ) {
int res = 18;
p.set_value(res);
}
int func2 ( std::future<int> f) {
int res=f.get();
return res;
}
int main () {
std::promise<int> p;
std::future<int> f = p.get_future();
std::future<void> fu1 = std::async(func1, std::move(p) );
std::future<int> fu2 = std::async(func2, std::move(f) );
int x = fu2.get();
return 0;
}
15
shared futures int factorial( std::shared_future<int> f ) {
int res = 1;
int N = f.get();
for ( int i=N; i> 1; i-- )
res *=i;
return res;
}
int main () {
std::promise<int> p;
std::future<int> f = p.get_future();
std::shared_future<int> sf = f.share();
std::future<int> fu = std::async(factorial, sf);
std::future<int> fu2 = std::async(factorial, sf);
std::future<int> fu3 = std::async(factorial, sf);
p.set_value(4);
int x = fu.get(); //
return 0;
}
shared future
• A shared_future object behaves like a future object,
except that it can be copied
• More than one shared_future can share ownership
over their end of a shared state.
• The value in the shared state can be retrieved multiple
times once ready.