Dealing with Mutation: Locking
Locking is a classical way to protect a shared, mutable state. Today, I will present the two variants, Scoped Locking and Strategized Locking.
Locking is a straightforward idea to protect a critical section. A critical section is a section of code that, at most, one thread can use at any time.
Scoped Locking
Scoped locking is the idea of RAII
applied to a mutex. Scoped locking is also known as synchronized block and guard. The key idea of this idiom is to bind the resource acquisition and release to an object’s lifetime. As the name suggests, the lifetime of the object is scoped. Scoped means that the C++ run time is responsible for object destruction and, therefore, for releasing the resource.
The class ScopedLock
implements Scoped Locking.
// scopedLock.cpp #include#include #include #include class ScopedLock{ private: std::mutex& mut; public: explicit ScopedLock(std::mutex& m): mut(m){ // (1) mut.lock(); // (2) std::cout << "Lock the mutex: " << &mut << 'n'; } ~ScopedLock(){ std::cout << "Release the mutex: " << &mut << 'n'; mut.unlock(); // (3) } }; int main(){ std::cout << 'n'; std::mutex mutex1; ScopedLock scopedLock1{mutex1}; std::cout << "nBefore local scope" << 'n'; { std::mutex mutex2; ScopedLock scopedLock2{mutex2}; } // (4) std::cout << "After local scope" << 'n'; std::cout << "nBefore try-catch block" << 'n'; try{ std::mutex mutex3; ScopedLock scopedLock3{mutex3}; throw std::bad_alloc(); } // (5) catch (std::bad_alloc& e){ std::cout << e.what(); } std::cout << "nAfter try-catch block" << 'n'; std::cout << 'n'; }
ScopedLock
gets its mutex by reference (line 1). The mutex is locked in the constructor (line 2) and unlocked in the destructor (line 3). Thanks to the RAII idiom, the object’s destruction and, therefore, the unlocking of the mutex is done automatically.
The scope of scopedLock1
ends at the end of the main function. Consequentially, mutex1
is unlocked. The same holds for mutex2
and mutex3
. They are automatically unlocked at the end of their local scopes (lines 4 and 5). For mutex3
, the destructor of the scopedLock3
is also invoked if an exception occurs. Interestingly, mutex3
reuses the memory of mutex2
because both have the same address.
Scoped locking has the following advantages and disadvantages.
- Advantages:
- Robustness, because the locks are automatically acquired and released
- Disadvantages:
- Recursive locking of a
std::mutex
is undefined behavior and may typically cause a deadlock - Locks are not automatically released if the c function
longjmp
is used;longjpm
does not call C++ destructors of scoped objects
- Recursive locking of a
C++17 supports locks in four variations. C++ has a std::lock_guard / std::scoped_lock
for the simple and a std::unique_lock / std::shared_lock
for the advanced use cases such as explicit locking or unlocking of the mutex. You can read more about mutex and locks in my previous post, “Prefer Locks to Mutexes“.
Strategized Locking often applies Scoped Locking.
Modernes C++ Mentoring
Be part of my mentoring programs:
-
-
-
-
-
-
- “Design Patterns and Architectural Patterns with C++” (open)
- “C++20: Get the Details” (July 2023)
-
-
-
-
-
Do you want to stay informed about my mentoring programs: Subscribe via E-Mail.
Strategized Locking
Assume you write code such as a library, which should be used in various domains, including concurrent ones. To be safe, you protect the critical sections with a lock. If your library now runs in a single-threaded environment, you have a performance issue because you implemented an expensive synchronization mechanism that is unnecessary. Now, Strategized Locking comes to your rescue. Strategized locking is the idea of the Strategy Pattern applied to locking. This means putting your locking strategy into an object and making it into a pluggable component of your system.
Two typical ways to implement Strategized Locking are run-time polymorphism (object orientation) or compile-time polymorphism (templates). Both ways improve the customization and extension of the locking strategy, ease the maintenance of the system, and support the reuse of components. Implementing the strategized locking at run-time or compile-time polymorphism differs in various aspects.
- Advantages:
- Run-time Polymorphism
- allows it to configure the locking strategy during run time.
- is easier to understand for developers who have an object-oriented background.
- Compile-Time Polymorphism
- has no abstraction penalty.
- has a flat hierarchy.
- Run-time Polymorphism
- Disadvantages:
- Run-time Polymorphism
- needs an additional pointer indirection.
- may have a deep derivation hierarchy.
- Compile-Time Polymorphism
- may generate a lengthy message in the error case, but using concepts such as
BasicLockable
in C++20 causes concise error messages.
- may generate a lengthy message in the error case, but using concepts such as
- Run-time Polymorphism
After this theoretical discussion, I will implement the strategized locking in both variations. The Strategized Locking supports, in my example, no-locking, exclusive locking, and shared locking. For simplicity reasons, I used internally already existing mutexes.
Run-Time Polymorphism
The program strategizedLockingRuntime.cpp
presents three different locks.
// strategizedLockingRuntime.cpp #include#include #include class Lock { // (4) public: virtual void lock() const = 0; virtual void unlock() const = 0; }; class StrategizedLocking { Lock& lock; // (1) public: StrategizedLocking(Lock& l): lock(l){ // (2) lock.lock(); } ~StrategizedLocking(){ // (3) lock.unlock(); } }; struct NullObjectMutex{ void lock(){} void unlock(){} }; class NoLock : public Lock { // (5) void lock() const override { std::cout << "NoLock::lock: " << 'n'; nullObjectMutex.lock(); } void unlock() const override { std::cout << "NoLock::unlock: " << 'n'; nullObjectMutex.unlock(); } mutable NullObjectMutex nullObjectMutex; // (10) }; class ExclusiveLock : public Lock { // (6) void lock() const override { std::cout << " ExclusiveLock::lock: " << 'n'; mutex.lock(); } void unlock() const override { std::cout << " ExclusiveLock::unlock: " << 'n'; mutex.unlock(); } mutable std::mutex mutex; // (11) }; class SharedLock : public Lock { // (7) void lock() const override { std::cout << " SharedLock::lock_shared: " << 'n'; sharedMutex.lock_shared(); // (8) } void unlock() const override { std::cout << " SharedLock::unlock_shared: " << 'n'; sharedMutex.unlock_shared(); // (9) } mutable std::shared_mutex sharedMutex; // (12) }; int main() { std::cout << 'n'; NoLock noLock; StrategizedLocking stratLock1{noLock}; { ExclusiveLock exLock; StrategizedLocking stratLock2{exLock}; { SharedLock sharLock; StrategizedLocking startLock3{sharLock}; } } std::cout << 'n'; }
The class StrategizedLocking
has a lock
(line 1). StrategizedLocking
models scoped locking and, therefore, locks the mutex in the constructor (line 2) and unlocks it in the destructor (line 3). Lock
(line 4) is an abstract class and defines all derived classes’ interfaces. These are the classes NoLock
(line 5), ExclusiveLock
(line 6), and SharedLock
(line 7). SharedLock
invokes lock_shared
(line 8) and unlock_shared
(line 9) on its std::shared_mutex
. Each of these locks holds one of the mutexes NullObjectMutex
(line 10), std::mutex
(line 11), or std::shared_mutex
(line 12). NullObjectMutex
is a noop placeholder. The mutexes are declared as mutable
. Therefore, they are usable in constant member functions such as lock
and unlock
.
Compile-Time Polymorphism
The template-based implementation is quite similar to the object-oriented-based implementation. Instead of an abstract base class Lock
, I define the concept BasicLockable
. If you need more information about concepts, read my previous posts: concepts.
template <typename T> concept BasicLockable = requires(T lo) { lo.lock(); lo.unlock(); };
BasicLockable
requires from its type parameter T
that it supports the member functions lock
and unlock
. Consequentially, the class template StrategizedLocking
accepts only constraints type parameters:
template <BasicLockable Lock> class StrategizedLocking { ...
Finally, here is the template-based implementation.
// strategizedLockingCompileTime.cpp #include#include #include template <typename T> concept BasicLockable = requires(T lo) { lo.lock(); lo.unlock(); }; template <BasicLockable Lock> class StrategizedLocking { Lock& lock; public: StrategizedLocking(Lock& l): lock(l){ lock.lock(); } ~StrategizedLocking(){ lock.unlock(); } }; struct NullObjectMutex { void lock(){} void unlock(){} }; class NoLock{ public: void lock() const { std::cout << "NoLock::lock: " << 'n'; nullObjectMutex.lock(); } void unlock() const { std::cout << "NoLock::unlock: " << 'n'; nullObjectMutex.lock(); } mutable NullObjectMutex nullObjectMutex; }; class ExclusiveLock { public: void lock() const { std::cout << " ExclusiveLock::lock: " << 'n'; mutex.lock(); } void unlock() const { std::cout << " ExclusiveLock::unlock: " << 'n'; mutex.unlock(); } mutable std::mutex mutex; }; class SharedLock { public: void lock() const { std::cout << " SharedLock::lock_shared: " << 'n'; sharedMutex.lock_shared(); } void unlock() const { std::cout << " SharedLock::unlock_shared: " << 'n'; sharedMutex.unlock_shared(); } mutable std::shared_mutex sharedMutex; }; int main() { std::cout << 'n'; NoLock noLock; StrategizedLocking<NoLock> stratLock1{noLock}; { ExclusiveLock exLock; StrategizedLocking<ExclusiveLock> stratLock2{exLock}; { SharedLock sharLock; StrategizedLocking<SharedLock> startLock3{sharLock}; } } std::cout << 'n'; }
The programs strategizedLockingRuntime.cpp
and strategizedLockingCompileTime.cpp
produce the same output:
What’s Next?
The Thread-Safe Interface extends the critical section to an object’s interface. I will present it in my next post.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, and Rob North.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, and Slavko Radman.
My special thanks to Embarcadero
My special thanks to PVS-Studio
My special thanks to Tipi.build
Seminars
I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.
Bookable (Online)
German
- Clean Code: Best Practices für modernes C++: 20.06.2023 – 22.06.2023 (Präsenzschulung)
- Design Pattern und Architekturpattern mit C++: 22.08.2023 – 24.08.2023 (Präsenzschulung)
- Embedded Programmierung mit modernem C++ 12.12.2023 – 14.12.2023 (Präsenzschulung)
Standard Seminars (English/German)
Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.
- C++ – The Core Language
- C++ – The Standard Library
- C++ – Compact
- C++11 and C++14
- Concurrency with Modern C++
- Design Pattern and Architectural Pattern with C++
- Embedded Programming with Modern C++
- Generic Programming (Templates) with C++
New
- Clean Code with Modern C++
- C++20
Contact Me
- Phone: +49 7472 917441
- Mobil:: +49 176 5506 5086
- Mail: This email address is being protected from spambots. You need JavaScript enabled to view it.
- German Seminar Page: www.ModernesCpp.de
- Mentoring Page: www.ModernesCpp.org
Modernes C++,