PERA LogoPERA Software Solutions GmbH
Compile time thread synchronization in C++

Compile time thread synchronization in C++


The usual way to synchronize thread access to shared data is by convention, which means the code contains a comment explaining that a certain mutex should be used to protect certain data: class Group { private: // Lock this mutex when accessing the names: std::mutex namesMutex_; std::vector< std::string > names_; }; But depending on the size of the header this comment might be overlooked and
gets accessed without protecting it by locking


To protect against such errors we need to ensure that
can only be used after its corresponding mutex is locked. We achieve this by putting the data to protect in the private section of a class declaration and get a pointer to it only when calling the
ensures that the mutex gets locked first and only then does it return the pointer: template < typename T > { class data_mutex { public: T *lock() { mutex_.lock(); return &data_; } void unlock() { mutex_.unlock(); } private: std::mutex mutex_; T data_; }; This design already ensures that
can only be accessed after
is called: class Group { public: void addName( const string &name ) { auto names = names_.lock(); names->push_back( name ); names_.unlock(); } private: data_mutex< std::vector< std::string >> names_; }; But there are of course at least 2 problems:
  1. Not exception safe.
  2. Can still forget to call
    at the end.
Interesting enough: Locking/unlocking is very similar to allocating/deallocating memory and we already have helper classes namely smart pointers (
) which help with freeing the memory. One very interesting feature of them is that it's possible to provide a custom deleter which makes it possible to specify how a pointer is getting deleted. In our case we can use it to automatically unlock the mutex after we are done working with the locked data: // Copyright 2015 Peter Most, PERA Software Solutions GmbH // // This file is part of the CppAidKit library. // // CppAidKit is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // CppAidKit is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with CppAidKit. If not, see <>. #pragma once #include <mutex> #include <memory> #include <functional> namespace pera_software { namespace aidkit { namespace concurrent { /// A compile time guaranteed mutex. /** * This special mutex guarantees that the embedded resource can only be accessed after the * associated mutex has been successfully locked. */ template < typename T, typename Mutex = std::recursive_mutex > class data_mutex { public: using native_handle_type = typename Mutex::native_handle_type; using pointer = std::unique_ptr< T, std::function< void ( T * ) >>; using const_pointer = std::unique_ptr< const T, std::function< void ( const T * ) >>; /// Initialize the embedded resource with the given parameters. template < typename ... Args > data_mutex( Args && ... args ) : data_( std::forward< Args >( args ) ... ) { lockCount_ = 0; } /// Lock the mutex and return a pointer for accessing the embedded resource. pointer lock() { mutex_.lock(); return make_pointer(); } pointer try_lock() noexcept { if ( mutex_.try_lock() ) return make_pointer(); else return pointer(); } const_pointer lock() const { mutex_.lock(); return make_const_pointer(); } const_pointer try_lock() const noexcept { if ( mutex_.try_lock() ) return make_const_pointer(); else return const_pointer(); } native_handle_type native_handle() { return mutex_.native_handle(); } // These methods follow the same idea then those in shared_ptr<> where unique() is // implemented in terms of use_count(): bool is_locked() const noexcept { return lock_count() > 0; } int lock_count() const noexcept { return lockCount_; } private: void unlock( const T *data_ptr ) const { // Protect against pointer.reset( some_pointer ): if ( data_ptr == &data_ ) { --lockCount_; mutex_.unlock(); } } // If a pointer is requested, then we know that locking has succeeded and we can // increment the lock counter: pointer make_pointer() noexcept { ++lockCount_; auto unlocker = [ = ]( T *data_ptr ) { unlock( data_ptr ); }; return pointer( &data_, unlocker ); } const_pointer make_const_pointer() const noexcept { ++lockCount_; auto unlocker = [ = ]( const T *data_ptr ) { unlock( data_ptr ); }; return const_pointer( &data_, unlocker ); } T data_; mutable Mutex mutex_; mutable int lockCount_; }; } } } Some important consequences of this design: