computer archicture f07 - uhgabriel/courses/cosc6374_f15/parco_16_c++threading1.pdf4 dealing with...

15
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

Upload: others

Post on 12-Oct-2020

0 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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

Page 2: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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;

}

Page 3: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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);

Page 4: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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

Page 5: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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;

}

Page 6: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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

}

Page 7: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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();

} }

Page 8: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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

Page 9: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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();

}

}

Page 10: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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;

}

Page 11: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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;

}

Page 12: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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;

}

Page 13: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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

Page 14: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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;

}

Page 15: Computer Archicture F07 - UHgabriel/courses/cosc6374_f15/ParCo_16_C++threading1.pdf4 Dealing with exception int main { std::thread t1(function_1, string(“1”)); // if main thread

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.