C/CPP Concurrency Notes
Page Contents
C++ Memory Model
Post C++11, the C++ memory model now officially supports threads.
C++ talks about its "world" in terms of a virtual machine, which works as an abstraction of whatever physical machine the program will be compiled to run on. As long as a C++ program is written to comply with the rules and restrictions of this virtual machine, the compiler guarantees to produce a program that will run correctly on the physical target it is compiling for.
Pre C++11 the memory model was single threaded, so by definition one couldn't talk about threads. Of course one
could still write threaded code using libraries such as pthreads
, they just weren't officially supported by
the standard so one could not be completely guaranteed that a program written in C++ could work on two different
targets.
With C+11 and beyond, as the memory model is multi-threaded, threads, locks, etc., etc., are supported
natively by the standard library, although the implementation may indeed just be pthreads
, or similar,
under the hood!
Start A Thread
Starting a thread is pretty easy...
join()
or detach()
from an std::thread
before it is destroyed, otherwise your program
will be restarted!
std::thread
is movable but not copyable.
As part of the above warning, when join()
'ing a thread, you must make sure no exceptions occur before you have done the join()
. Use RAII to combat this!
Using A Function
#include <iostream>
#include <thread>
void my_thread_function(int a, int b)
{
std::cout << "This is my thread running: " << a << " - " << b << "\n";
}
int main(int argc, char *argv[])
{
std::thread my_thread(my_thread_function, 9, 2);
my_thread.join();
return 0;
}
Using A Function Pointer To A Class Member Function
#include <iostream>
#include <thread>
class MyThreadyThing
{
public:
void MyThreadyFunction(int a, int b)
{
std::cout << "This is my thread running: " << a << " - " << b << "\n";
}
};
int main(int argc, char *argv[])
{
MyThreadyThing thing;
std::thread my_thread(&MyThreadyThing::MyThreadyFunction, &thing, 99, 22);
my_thread.join();
return 0;
}
Using A Callable
#include <iostream>
#include <thread>
class my_thread_callable
{
public:
void operator() (int a, int b) const
{
std::cout << "This is my thread running: " << a << " - " << b << "\n";
}
};
int main(int argc, char *argv[])
{
constexpr bool use_temporary = true;
if constexpr (use_temporary)
{
// For a temporary must use brace intialiser to avoid most vexing parse issue.
std::thread my_thread{my_thread_callable(), 20, 21};
my_thread.join();
}
else {
my_thread_callable my_thread_obj;
std::thread my_thread(my_thread_obj, 19, 20);
my_thread.join();
}
return 0;
}
Thread Parameter Gotchas
The std::thread
constructor copies supplied values. Thus, if the thread function expects references it will
get a reference to the copy, not the original.
The solution is to wrap the argument using std::ref()
. This will wrap the object with an appropriate
std::reference_wrapper
type. This is an object that emulates a reference, internally holding a reference to
your object. Thus when std::thread
copies this object, the internal reference used will still reference
your original object and not a copy.
Semaphores
Interestingly C++ didn't get a semaphore class until C++20. Prior to that there were only mutexes and condition variables [Ref].
Mutexes
- Create using
std::mutex
. Acquire/lock with.lock()
and release/unlock with.unlock()
, however to avoid forgetting to unlock, best to use RAII in the form of astd::lock_guard<std::mutex>
.
std::mutex my_mutex;
void do_something(void) {
std::lock_guard<std::mutex> guard(my_mutex);
// Rest of function until `guard` goes out of scope is now protected by `my_mutex`
// ....
}
Deadlock
std::lock
- When locks cannot be reliably acquired in the same order use
std::lock()
- it can lock two or more mutexes at once without risk of deadlock.