template: titleslide
-
Introduction
-
Creating and joining threads
-
Passing arguments to threads
-
Synchronisation
- API for multithreaded programming built in to C++11 standard
- Similar functionality to POSIX threads
- but with a proper OO interface
- based quite heavily on Boost threads library
- Portable
- depends on C++11 support, available in most compilers today
- Threads are C++ objects
- call a constructor to create a thread
- Synchronisation
- mutex locks
- condition variables
- C++11 atomics
- Tasks
- via async/futures/promises
-
Threads are objects of the
std::thread
class -
Threads are created by calling the constructor for this class
-
Pass as an argument what we want the thread to execute. This can be:
- A function pointer
- A function object / functor
- A lambda expression
-
Note: you cannot copy a thread
The join()
member function on a std::thread
object causes the calling thread to wait for the thread object to finish executing its function/functor/lambda.
#include <thread>
#include <iostream>
#include <vector>
void hello() {
std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.push_back(std::thread(hello));
}
for (auto& thread: threads) {
thread.join();
}
}
#include <thread>
#include <iostream>
#include <vector>
int main() {
std::vector<std::thread> threads;
for(int i = 0; i < 5; ++i){
threads.push_back(std::thread([] {
std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;
}));
}
for(auto& thread : threads){
thread.join();
}
}
- Can call
get_id()
on a thread - Use
std::this_thread::get_id()
to call it on the executing thread - Returns an arbitrary identifier
- Not much use!
- If we want sequentially numbered threads, need to pass the number as an argument to the thread constructor.
Arguments to the thread function are moved or copied by value
Passing simple arguments to threads is straightforward:
void hello(int x, int y) {
std::cout << "Hello " << x << " " << y << std::endl;
}
int main() {
int a = 1;
int b = 27;
std::thread mythread(hello, a, b);
mythread.join();
}
Need to use a reference wrapper to avoid the argument to the thread constructor making a copy
void hello(int& x) {
x++;
}
int main() {
int x = 9;
std::thread mythread(hello, std::ref(x));
mythread.join();
std::cout << "x = " << x << std::endl; // x is 10 here
}
class Wallet
{
int mMoney = 0;
public:
Wallet() {}
void addMoney(int money) {
mMoney += money;
}
};
If two threads call addMoney()
on the same Wallet
object, then we have a race condition.
- Can use a mutex lock to protect updates to shared variables
- natural to declare a mutex inside the object whose data needs protecting
#include <mutex>
class Wallet
{
int mMoney = 0;
std::mutex mutex;
public:
Wallet() {}
void addMoney(int money) {
mutex.lock();
mMoney += money;
mutex.unlock();
}
};
- Need to make sure a mutex is always unlocked
- Can be tricky in cases with complex control flow, or with exception handling.
- The
std::lock_guard
class implements the RAII (resource allocation is initialization) pattern for mutexes - Its constructor takes as an argument a mutex, which it then locks
- Its destructor unlocks the mutex
#include <mutex>
class Wallet
{
int mMoney = 0;
std::mutex mutex;
public:
Wallet() {}
void addMoney(int money) {
std::lock_guard<std::mutex> lockGuard(mutex);
mMoney += money;
} // mutex unlocked when lockGuard goes out of scope
};
-
C++ provides an atomic template class
std::atomic
-
Efficient, lock-free operations supported for specialization to basic integer, boolean and character types
-
Floating point support in C++20 standard only
#include <atomic>
class Wallet
{
std::atomic<int> mMoney = 0;
public:
Wallet() {}
void addMoney(int money) {
mMoney += money; //atomic increment
}
};
-
Possible to add a mutex data member to the class and make every member function that accesses any mutable state acquire and release the mutex (use a
lock_guard
) -
Good design, in the sense that multithreaded code can use the class without worrying about the synchronisation
-
Can result in unacceptable overheads – lots of lock/unlocks, and synchronization when it’s not needed.
-
Need to think carefully about use cases in a given application.
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
https://creativecommons.org/licenses/by-nc-sa/4.0/
.smaller[ This means you are free to copy and redistribute the material and adapt and build on the material under the following terms: You must give appropriate credit, provide a link to the license and indicate if changes were made. If you adapt or build on the material you must distribute your work under the same license as the original.
Acknowledge EPCC as follows: “© EPCC, The University of Edinburgh, www.epcc.ed.ac.uk”
Note that this presentation may contain images owned by others. Please seek their permission before reusing these images. ]