Dealing with Mutation: Thread-Safe Interface
I continue my journey with concurrency patterns in today’s post. The Thread-Safe Interface fits very well when the critical sections are just objects.
The naive idea to protect all member functions of a class with a lock causes, in the best case, a performance issue and, in the worst case, a deadlock.
A Deadlock
The small code snippet has a deadlock.
struct Critical{ void memberFunction1(){ lock(mut); memberFunction2(); ... } void memberFunction2(){ lock(mut); ... } mutex mut; }; Critical crit; crit.memberFunction1();
Calling crit.memberFunction1
causes the mutex mut
to be locked twice. For simplicity reasons, the lock is a scoped lock. Here are the two issues:
- When
lock
is a recursive lock, the secondlock(mut)
inmemberFunction2
is redundant. - When
lock
is a non-recursive lock, the secondlock(mut)
inmemberFunction2
leads to undefined behavior. Most of the time, you get a deadlock.
The thread-safe interface overcomes both issues.
The Thread-Safe Interface
Here is the straightforward idea of the Thread-Safe Interface.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
- All interface member functions (
public
) use a lock. - All implementation member functions (
protected
andprivate
) must not use a lock. - The interface member functions call only protected or private member functions but no public member functions.
The following program shows the usage of the Thread-Safe Interface.
// threadSafeInterface.cpp #include <iostream> #include <mutex> #include <thread> class Critical{ public: void interface1() const { std::lock_guard<std::mutex> lockGuard(mut); implementation1(); } void interface2(){ std::lock_guard<std::mutex> lockGuard(mut); implementation2(); implementation3(); implementation1(); } private: void implementation1() const { std::cout << "implementation1: " << std::this_thread::get_id() << '\n'; } void implementation2(){ std::cout << " implementation2: " << std::this_thread::get_id() << '\n'; } void implementation3(){ std::cout << " implementation3: " << std::this_thread::get_id() << '\n'; } mutable std::mutex mut; // (1) }; int main(){ std::cout << '\n'; std::thread t1([]{ const Critical crit; crit.interface1(); }); std::thread t2([]{ Critical crit; crit.interface2(); crit.interface1(); }); Critical crit; crit.interface1(); crit.interface2(); t1.join(); t2.join(); std::cout << '\n'; }
Three threads, including the main thread, use instances of Critical
. Thanks to the Thread-Safe Interface, all calls to the public API are synchronized. The mutex mut
in line (1) is mutable and can be used in the constant member function interface1
.
The advantages of the thread-safe interface are threefold.
- A recursive call of a mutex is not possible. Recursive calls on a non-recursive mutex are undefined behavior in C++ and usually end in a deadlock.
- The program uses minimal locking and, therefore, minimal synchronization. Using just a
std::recursive_mutex
in each member function of the classCritical
would end in more expensive synchronization. - From the user’s perspective,
Critical
is straightforward to use because synchronization is only an implementation detail.
Each interface member function delegates its work to the corresponding implementation member function. The indirection overhead is a typical disadvantage of the Thread-Safe Interface.
The output of the program shows the interleaving of the three threads.
Although the Thread-Safe Interface seems easy to implement, there are two grave perils you have to keep in mind.
Perils
Using a static member in your class or having virtual interfaces requires special care.
Static members
When your class has a static member that is not constant, you must synchronize all member function calls on the class instances.
class Critical{ public: void interface1() const { std::lock_guard<std::mutex> lockGuard(mut); implementation1(); } void interface2(){ std::lock_guard<std::mutex> lockGuard(mut); implementation2(); implementation3(); implementation1(); } private: void implementation1() const { std::cout << "implementation1: " << std::this_thread::get_id() << '\n'; ++called; } void implementation2(){ std::cout << " implementation2: " << std::this_thread::get_id() << '\n'; ++called; } void implementation3(){ std::cout << " implementation3: " << std::this_thread::get_id() << '\n'; ++called; } inline static int called{0}; // (1) inline static std::mutex mut; };
Now, the class Critical
has a static member called
(line 32) to count how often the implementation functions were called. All instances of Critical
use the same static member and have, therefore, to be synchronized. Since C++17, static data members can be declared inline. An inline static data member can be defined and initialized in the class definition.
Virtuality
When you override a virtual interface function, the overriding function should have a lock even if the function is private.
// threadSafeInterfaceVirtual.cpp #include <iostream> #include <mutex> #include <thread> class Base{ public: virtual void interface() { std::lock_guard<std::mutex> lockGuard(mut); std::cout << "Base with lock" << '\n'; } virtual ~Base() = default; private: std::mutex mut; }; class Derived: public Base{ void interface() override { std::cout << "Derived without lock" << '\n'; } }; int main(){ std::cout << '\n'; Base* base1 = new Derived; base1->interface(); Derived der; Base& base2 = der; base2.interface(); std::cout << '\n'; }
In the calls, base1->interface
and base2.interface
the static type of base1
and base2
is Base,
and, therefore, the interface
is accessible. Because the interface member function is virtual, the call happens at run time using the dynamic type Derived
. At last, the private member function interface
of the class Derived
is invoked.
The program’s output shows the unsynchronized invocation of Derived’s interface function.
There are two typical ways to overcome this issue.
- Make the member function interface a non-virtual member function. This technique is called NVI (Non-Virtual Interface). The non-virtual member function guarantees that the interface function of the base class
Base
is used. Additionally, overriding the interface function usingoverride
causes a compile-time error because there is nothing to override. - Declare the member function interface as
final
:virtual void interface() final
. Thanks tofinal
, overriding an asfinal
declared virtual member function causes a compile-time error.
Although I presented two ways to overcome the challenges of virtuality, I strongly suggest using the NVI idiom. Use early binding if you don’t need late binding (virtuality). You can read more about NVI in my post:The Template Method.
What’s Next?
Guarded Suspension applies a different strategy to deal with mutation. It signals when it is done with its mutation. In my next post, I will write about Guarded Suspension.
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, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, 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, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!