📜 ⬆️ ⬇️

Rust Style C ++ Mutexes

Hello Habrahabr!

I often develop programs in C ++ and I love this language, no matter what they say about it. Probably because in many areas there is no replacement for it. However, this language, as we all know, is not without flaws, and therefore I always follow with interest the new approaches, patterns, or even programming languages, designed to solve some of these problems.

So, recently, I looked with interest at Stepan Koltsov’s stepancheg presentation on the Rust programming language, and I really liked the idea of ​​implementing myutexes in this language. Moreover, I did not see any obstacles to the implementation of such a primitive in C ++ and immediately opened the IDE, in order to put this into practice.

Immediately I warn you that I will write using the C ++ 11 standard, therefore, if you are going to compile the proposed code, you should do it with the -std=c++11 flag. I also want to immediately warn you that I do not pretend to originality, and I fully admit that such a primitive already exists in any library or framework.
')
So let's get started. To begin with, let's imagine what we want to get in the end. The same mutex in Rust, which will serve as a prototype for us, works as follows: This is a template class that is parameterized by the type of data that it must protect. That is, in fact, it is not a mutex in the sense to which we are all accustomed, but rather some kind of protected resource that contains not only the mutex itself, but also the value it protects. At the same time, this type is designed in such a way that access to protected data was in principle impossible without prior capture of a mutex.

Thus, the result should be a template class, parameterized by type, with the properties described above. Let's call it conditionally SharedResource. Working with him should end up looking something like this:
Show code
 SharedResource<int> shared_int(5); // ... // -   { //      , //         auto shared_int_accessor = shared_int.lock(); *shared_int_accessor = 10; //   ,  shared_int_accessor //        //   } 

As you can see, everything is simple, but safe. With this approach, it is impossible to forget to capture the mutex when accessing the protected resource or release it after all operations with the shared resource are completed. Let's get down to implementation. So let's start with the routine: the stub class itself, containing standard constructors, destructors, and operators.
Show code
 template<typename T> class SharedResource { public: SharedResource() = default; ~SharedResource() = default; SharedResource(SharedResource&&) = delete; SharedResource(const SharedResource&) = delete; SharedResource& operator=(SharedResource&&) = delete; SharedResource& operator=(const SharedResource&) = delete; private: }; 

So far everything is trite. For now, copying and moving will be prohibited, then we will change this if the need or desire arises. But we still have not implemented the features described even here in this line of our usage example:
 template<typename T> class SharedResource SharedResource<int> shared_int(5); 

We are not able to initialize the resource we are protecting. Let's try to fix it. When using the C ++ 03 standard or below, it would be quite problematic for us (although it is possible) for obvious reasons - the designers of the protected resource can take any number of arguments of arbitrary types. However, with the advent in C ++ 11 Variadic Templates, this problem has disappeared. All the functionality we need is implemented easily and simply as follows:
Show code
 template<typename T> class SharedResource { public: template<typename ...Args> SharedResource(Args ...args) : m_resource(args...) { } ~SharedResource() = default; SharedResource(SharedResource&&) = delete; SharedResource(const SharedResource&) = delete; SharedResource& operator=(SharedResource&&) = delete; SharedResource& operator=(const SharedResource&) = delete; private: T m_resource; }; 

Now we have the m_resource field in the class - this is the same resource we are protecting. And now we can initialize it in any way we like. It remains only to realize the possibility of seizing control over the resource and gaining access to it — that is, the most interesting. Let's get started:
Show code
 #include <mutex> template<typename T> class SharedResource { public: template<typename ...Args> SharedResource(Args ...args) : m_resource(args...) { } ~SharedResource() = default; SharedResource(SharedResource&&) = delete; SharedResource(const SharedResource&) = delete; SharedResource& operator=(SharedResource&&) = delete; SharedResource& operator=(const SharedResource&) = delete; class Accessor { friend class SharedResource<T>; public: ~Accessor() { m_shared_resource.m_mutex.unlock(); } private: Accessor(SharedResource<T> &resource) : m_shared_resource(resource) { m_shared_resource.m_mutex.lock(); } SharedResource<T> &m_shared_resource; }; Accessor lock() { return Accessor(*this); } private: T m_resource; std::mutex m_mutex; }; 

So, as we see, we have a new class - SharedResource :: Accessor. This class is the same proxy that provides access to a shared resource while it is captured. The SharedResource class is declared friendly to it so that this class can be called by its constructor. The important point is that no one except the parent class can create instances of this class directly. The only way to do this is to call the SharedResource :: lock () method. We also see that when constructing an instance of this class, a mutex is captured, and if destroyed, it is released. Everything is clear here - we want the mutex for the resource to be captured all the time while we have access to it, the presence of which should be provided by the SharedResource :: Accessor class.

However, in the current state, the class is very insecure. It is about copying or moving instances of this class. Neither the former nor the latter are not explicitly declared, which means that constructors and operators will be used by default. In this case, they will work incorrectly - for example, when copying, the mutex will not be captured again (which is correct), but it will be released upon destruction. Thus, if an instance of the class is copied, the mutex will be released one time more than it was captured, and we get our favorite Undefined Behavior. Let's try to fix this:
Show code
 #include <mutex> template<typename T> class SharedResource { public: template<typename ...Args> SharedResource(Args ...args) : m_resource(args...) { } ~SharedResource() = default; SharedResource(SharedResource&&) = delete; SharedResource(const SharedResource&) = delete; SharedResource& operator=(SharedResource&&) = delete; SharedResource& operator=(const SharedResource&) = delete; class Accessor { friend class SharedResource<T>; public: ~Accessor() { if (m_shared_resource) { m_shared_resource->m_mutex.unlock(); } } Accessor(const Accessor&) = delete; Accessor& operator=(const Accessor&) = delete; Accessor(Accessor&& a) : m_shared_resource(a.m_shared_resource) { a.m_shared_resource = nullptr; } Accessor& operator=(Accessor&& a) { if (&a != this) { if (m_shared_resource) { m_shared_resource->m_mutex.unlock(); } m_shared_resource = a.m_shared_resource; a.m_shared_resource = nullptr; } return *this; } private: Accessor(SharedResource<T> *resource) : m_shared_resource(resource) { m_shared_resource->m_mutex.lock(); } SharedResource<T> *m_shared_resource; }; Accessor lock() { return Accessor(this); } private: T m_resource; std::mutex m_mutex; }; 

We banned copying, but allowed the move. The negative consequence of this decision was that our proxy can now be invalid (after moving), and it can not be used to gain access to the resource. This is not very good, but not fatal - the displaced objects are not intended for further use. In addition, crashes when using such objects will be reproduced in 100% of cases, thanks to nullptr, which makes the detection of such errors not very difficult in most cases. However, it would be nice to give the user the opportunity to check the object for validity. Let's do this by adding this method:
Show code
 bool isValid() const noexcept { return m_shared_resource != nullptr; } 

Now the user can always check his copy of proxy for validity. Optionally, you can add an operator bool, although I did not do that. So, it remains to implement only the very access to the shared resource. We do this by adding the following operators for the SharedResource :: Accessor class:
Show code
 T* operator->() { return &m_shared_resource->m_resource; } T& operator*() { return m_shared_resource->m_resource; } 

Completely class will look like this:
Show code
 #include <mutex> template<typename T> class SharedResource { public: template<typename ...Args> SharedResource(Args ...args) : m_resource(args...) { } ~SharedResource() = default; SharedResource(SharedResource&&) = delete; SharedResource(const SharedResource&) = delete; SharedResource& operator=(SharedResource&&) = delete; SharedResource& operator=(const SharedResource&) = delete; class Accessor { friend class SharedResource<T>; public: ~Accessor() { if (m_shared_resource) { m_shared_resource->m_mutex.unlock(); } } Accessor(const Accessor&) = delete; Accessor& operator=(const Accessor&) = delete; Accessor(Accessor&& a) : m_shared_resource(a.m_shared_resource) { a.m_shared_resource = nullptr; } Accessor& operator=(Accessor&& a) { if (&a != this) { if (m_shared_resource) { m_shared_resource->m_mutex.unlock(); } m_shared_resource = a.m_shared_resource; a.m_shared_resource = nullptr; } return *this; } bool isValid() const noexcept { return m_shared_resource != nullptr; } T* operator->() { return &m_shared_resource->m_resource; } T& operator*() { return m_shared_resource->m_resource; } private: Accessor(SharedResource<T> *resource) : m_shared_resource(resource) { m_shared_resource->m_mutex.lock(); } SharedResource<T> *m_shared_resource; }; Accessor lock() { return Accessor(this); } private: T m_resource; std::mutex m_mutex; }; 

Is done. All the basic functionality for this class is implemented and the class is ready for use. Of course, it would be nice to implement also an analogue of the Rust method new_with_condvars, which, when creating a class, associates a mutex with a transmitted list of condition variables (condvars). In C ++, mutexes and condition variables are bound differently when waiting on a condvar instance. To do this, an instance of the unique_lock class is passed to the condition_variable :: wait method, which is an abstraction of owning a mutex without providing access to the resource.

It would be possible to change our implementation so that interaction with condvars is possible, but I am afraid that in this case the implementation will cease to be simple and reliable, and in fact this was the original idea. However, those who wish can find the implementation that works with condvars below.
Show code
 #include <mutex> template<typename T> class SharedResource { public: template<typename ...Args> SharedResource(Args ...args) : m_resource(args...) { } ~SharedResource() = default; SharedResource(SharedResource&&) = delete; SharedResource(const SharedResource&) = delete; SharedResource& operator=(SharedResource&&) = delete; SharedResource& operator=(const SharedResource&) = delete; class Accessor { friend class SharedResource<T>; public: ~Accessor() = default; Accessor(const Accessor&) = delete; Accessor& operator=(const Accessor&) = delete; Accessor(Accessor&& a) : m_lock(std::move(a.m_lock)), m_shared_resource(a.m_shared_resource) { a.m_shared_resource = nullptr; } Accessor& operator=(Accessor&& a) { if (&a != this) { m_lock = std::move(a.m_lock); m_shared_resource = a.m_shared_resource; a.m_shared_resource = nullptr; } return *this; } bool isValid() const noexcept { return m_shared_resource != nullptr; } T* operator->() { return m_shared_resource; } T& operator*() { return *m_shared_resource; } std::unique_lock<std::mutex>& get_lock() noexcept { return m_lock; } private: Accessor(SharedResource<T> *resource) : m_lock(resource->m_mutex), m_shared_resource(&resource->m_resource) { } std::unique_lock<std::mutex> m_lock; T *m_shared_resource; }; Accessor lock() { return Accessor(this); } private: T m_resource; std::mutex m_mutex; }; 

On this we can consider the implementation of our class completed.
Now a couple of comments on how this class cannot be used in order not to shoot yourself a leg or something else vital:


Many thanks to all for your attention.
Link to github code: https://github.com/isapego/shared-resource .
The code is published under the public domain license, so you can do with it everything that you just come to mind.
I would be glad if the article would be useful to someone.

PS
Thanks to everyone who pointed out errors, gave advice here and on the githaba and helped to make the article and code better.

Source: https://habr.com/ru/post/237941/


All Articles