Dealing with Mutation: Thread-Safe Interface

Contents[Show]

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.

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

 

Rainer D 6 P2 540x540Modernes C++ Mentoring

Be part of my mentoring programs:

 

 

 

 

Do you want to stay informed about my mentoring programs: Subscribe via E-Mail.

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, 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 CBUIDER STUDIO FINAL ICONS 1024 Small

 

My special thanks to PVS-Studio PVC Logo

 

My special thanks to Tipi.build tipi.build logo

 

My special thanks to Take Up Code TakeUpCode 450 60

 

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

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

Modernes C++,

RainerGrimmDunkelBlauSmall

 

Stay Informed about my Mentoring

 

Mentoring

English Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Course: The All-in-One Guide to C++20

Course: Master Software Design Patterns and Architecture in C++

Subscribe to the newsletter (+ pdf bundle)

All tags

Blog archive

Source Code

Visitors

Today 4616

Yesterday 6193

Week 10809

Month 32483

All 12110692

Currently are 179 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments