DealingWithMutation

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.

 DealingWithMutation

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 second lock(mut) in memberFunction2 is redundant.
  • When lock is a non-recursive lock, the second lock(mut) in memberFunction2 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.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    • All interface member functions (public) use a lock.
    • All implementation member functions (protected and private) 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.

    1. 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.
    2. The program uses minimal locking and, therefore, minimal synchronization. Using just a std::recursive_mutex in each member function of the class Critical would end in more expensive synchronization.
    3. 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.

    threadSafeInterface

    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.

    threadSafeInterfaceVirtual

    There are two typical ways to overcome this issue.

    1. 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 using override causes a compile-time error because there is nothing to override.
    2. Declare the member function interface as final: virtual void interface() final. Thanks to final, overriding an as final 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,and Matt Godbolt.

    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)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *