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...

You must 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

See it on Github

#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

See it on Github

#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

See it on Github

#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 a std::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.

std::unique_lock