4 Easy Tips for Using Threads and Mutexes in C++
By: Joshua Weinstein
Tags: Software Engineering, Programming, Technology, Software Development

3. Use a mutex When Threads Are Accessing Shared Resources and Data

In many multithreaded programs and designs, threads will need to read, write, and exchange data with other threads. Threads can share any resource that is not local to each thread. So variables and objects allocated on a thread’s stack won’t be accessible to other threads. Global objects, or pointers to objects passed into a thread’s initial “main” function, can be accessed by multiple threads. However, to have a fully functioning, bug-free program, you need to ensure threads share resources safely. If one thread reads a resource, like an array or chunk of memory, while another one is writing to it, that’s going to result in corrupted data.

There are many tools and techniques that can be used to achieve safe resource sharing, but the most straightforward tool to accomplish this is a mutex. A mutex is an object with a hardware- and system-specific implementation that allows a thread to acquire a “lock.” While the thread holds the mutex, other threads who attempt to acquire the mutex will halt or block the acquisition call. This means other threads cannot proceed to lock the mutex until the thread holding it currently unlocks it. The C++11 standard defines a cross-platform type called std::mutex that can be locked or unlocked by different threads. It’s as simple as doing:

Although that code does lock and unlock a mutex, directly calling methods on the mutex — such as the lock and unlock methods — is generally not recommended. That’s because as soon as .unlock() is called, any thread waiting on that mutex will be given access to that portion of code between the lock and unlock statements. Additionally, return statements before the .unlock() statement will cause the mutex to stay locked forever, which is a very dangerous situation.

The most common way to utilize a mutex is using an RAII style object that’s constructed at the top of a scope and destroyed at the end. For C++11, the standard library provides something called std::lock_guard . In C++17, there’s an even better class called std::scoped_lock , which is an RAII object that can lock multiple mutexes at once in a scope. The advantage of using an RAII scoped approach to mutex locking is the unlock is handled when the scope is finished, so you don’t have to worry about the order of your statements or having to reach an explicit call to .unlock() .

For this example, two different threads will read and write values from an array, and that array will be guarded by a mutex. One thread will have the role of a writer, whereas the other one will have the role of the reader. The threads will be controlled via a global flag, as before. Let’s take a look at the full code:

The first thing you might notice is the passing by value of the Point objects. Typically in C++, you would pass by reference when accessing an element of a vector or array. However, as soon as the any function with a lock guard at the top of its scope returns, that mutex is unlocked. You cannot safely return a pointer or reference to a resource that is shared by multiple threads directly. That’s because acquiring a pointer has no guarantee about blocking other threads from changing the value it points to. That’s only controlled by a mutex.

In the example above, you will likely notice several sleep_for calls. These aren’t required to make the code work, but using them and altering their periods can help visualize threads doing work, going to sleep, then waking up again. If you run this program and decrease the sleep period for just the writer thread, you might see a pattern like the following:

x=55 y=55

x=56 y=56

x=57 y=57

x=58 y=58

x=59 y=59

x=80 y=80

x=81 y=81

x=82 y=82

x=83 y=83

x=84 y=84

x=85 y=85

There’s a skip in the printouts! But this is not a bug. If the reader thread sleeps for a longer period of time than the writer thread, that means the writer thread will have the time to do work, while the reader thread is sleeping. This means by the time the reader thread wakes up, the values in the array are advanced past the point to which it last read them, rendering its observation no longer sequential. When the two sleep periods are equal, you shouldn’t see this happening.