PERA Software Solutions GmbH

Compile time guaranteed thread synchronization in C++

Compile time guaranteed thread synchronization in C++

Problem

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: // ExplicitLockedImageCache.cpp class ExplicitLockedImageCache { public: shared_ptr<Image> loadImage(const string &name) { lock_guard lock(cachedImagesMutex); auto position = cachedImages.find(name); if (position == cachedImages.end()) { shared_ptr<Image> image = loadImageFromFile(name); cachedImages.insert({name, image}); return image; } else { return position->second; } } void removeImage(const string &name) { auto position = cachedImages.find(name); if (position != cachedImages.end()) { cachedImages.erase(position); } } private: // Lock this mutex before accessing the cached images! mutex cachedImagesMutex; map<string, shared_ptr<Image>> cachedImages; }; But if you are editing in the source file, you might not even have seen that comment and
cachedImages
gets accessed without protecting it by locking
cachedImagesMutex
, as has already happened in
removeImage()
!

Approach

To protect against such errors we need to ensure that
cachedImages
can only be used after its associated mutex has been locked. We achieve this by putting the data and the mutex together in the private section of a class/template declaration. To gain access to the data, we have to use a
data_mutex_ptr
/
const_data_mutex_ptr
, just like
lock_guard<>
. The constructor of
data_mutex_ptr
will lock the mutex and the desctructor will unlock it. So the usage then looks like this: // ImplicitLockedImageCache.cpp class ImplicitLockedImageCache { public: shared_ptr<Image> loadImage(const string &name) { data_mutex_ptr images(&cachedImages); auto position = images->find(name); if (position == images->end()) { shared_ptr<Image> image = loadImageFromFile(name); images->insert({name, image}); return image; } else { return position->second; } } void removeImage(const string &name) { data_mutex_ptr images(&cachedImages); auto position = images->find(name); if (position != images->end()) { images->erase(position); } } private: data_mutex<map<string, shared_ptr<Image>>> cachedImages; };

Implementation

This is the
data_mutex<T>
version which you can find in my CppAidKit library. It contains the aforementioned
data_mutex
,
data_mutex_ptr
and
const_data_mutex_ptr
: // Copyright 2019 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 <http://www.gnu.org/licenses/>. #pragma once #include <mutex> namespace pera_software::aidkit::concurrent { template <typename T, typename Mutex> class data_mutex_ptr; template <typename T, typename Mutex> class const_data_mutex_ptr; template <typename T, typename Mutex = std::mutex> class data_mutex { public: template <typename... Args> data_mutex(Args &&... args) : data_(std::forward<Args>(args)...) { } data_mutex(const data_mutex &) = delete; data_mutex &operator=(const data_mutex &) = delete; private: friend data_mutex_ptr<T, Mutex>; friend const_data_mutex_ptr<T, Mutex>; T data_; mutable Mutex mutex_; }; template <typename T, typename Mutex = std::mutex> class data_mutex_ptr { public: explicit data_mutex_ptr(data_mutex<T, Mutex> *dataMutex) noexcept : data_(&dataMutex->data_), mutex_(&dataMutex->mutex_) { mutex_->lock(); } ~data_mutex_ptr() noexcept { mutex_->unlock(); } T *operator->() noexcept { return data_; } T &operator*() noexcept { return *data_; } data_mutex_ptr(const data_mutex_ptr &) = delete; data_mutex_ptr &operator=(const data_mutex_ptr &) = delete; private: T *data_; mutable Mutex *mutex_; }; // Provide class template argument deduction guide (CTAD) to silence the warning: // "'data_mutex_ptr' may not intend to support class template argument deduction [-Wctad-maybe-unsupported]" template <typename T, typename Mutex> data_mutex_ptr(data_mutex<T, Mutex>) -> data_mutex_ptr<T, Mutex>; template <typename T, typename Mutex = std::mutex> class const_data_mutex_ptr { public: explicit const_data_mutex_ptr(const data_mutex<T, Mutex> *dataMutex) noexcept : data_(&dataMutex->data_), mutex_(&dataMutex->mutex_) { mutex_->lock(); } ~const_data_mutex_ptr() noexcept { mutex_->unlock(); } const T *operator->() const noexcept { return data_; } const T &operator*() const noexcept { return *data_; } const_data_mutex_ptr(const const_data_mutex_ptr &) = delete; const_data_mutex_ptr &operator=(const const_data_mutex_ptr &) = delete; private: const T *data_; mutable Mutex *mutex_; }; // Provide class template argument deduction guide (CTAD) to silence the warning: // "'const_data_mutex_ptr' may not intend to support class template argument deduction [-Wctad-maybe-unsupported]" template <typename T, typename Mutex> const_data_mutex_ptr(data_mutex<T, Mutex>) -> const_data_mutex_ptr<T, Mutex>; }