DealingWithMutation

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.

DealingWithMutation

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 <iostream>
#include <mutex>
#include <new>
#include <string>

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.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    scopedLock

    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

    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.

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

    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 <iostream>
    #include <mutex>
    #include <shared_mutex>
    
    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 <iostream>
    #include <mutex>
    #include <shared_mutex>
    
    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:
    strategizedLocking

    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, 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, 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, moon, Philipp Lenk, Hobsbawm, and Charles-Jianye Chen.

    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

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    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++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    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 *