Rust Mutexes for C ++
Hello Habrahabr!
I often develop programs in C ++ and love this language, no matter what they say. Probably because in many areas he has not yet been replaced. 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 watched with interest the presentation of Stepan Koltsov stepancheg about the Rust programming language, and I really liked the idea of implementing mutexes in this language. And I did not see any obstacles to the implementation of such a primitive in C ++ and immediately opened the IDE, in order to implement this in practice.
I warn you right away that I will write using the C ++ 11 standard, so if you intend to compile the proposed code, you should do this with the flag
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 our prototype, works as follows: This is a template class that is parameterized by the type of data that it should protect. That is, in fact, this is not a mutex in the sense that we are all used to, but rather a kind of protected resource that contains not only the mutex itself, but also the value that it protects. Moreover, this type is designed in such a way that access to the protected data was in principle impossible without preliminary capture of the mutex.
Thus, the result should be a template class parameterized by the type that has the properties described above. Let's call it conditionally SharedResource. Work with him should eventually look something like this:
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 to release it after all operations with the shared resource have been completed. Let's get started. So, let's start with a routine: the stub class itself, which contains standard constructors, destructors, and operators.
So far, everything is corny. Copying and moving is prohibited for now, then we will change it if a need or desire arises. But we still haven’t realized the capabilities described even in this line of our usage example:
We do not have the ability to initialize the resource we protect. Let's try to fix it. When using the standard C ++ 03 or lower, it would be rather problematic (although possible) for the obvious reason - the designers of the protected resource can accept any number of arguments of arbitrary types. However, with the introduction of Variadic Templates in C ++ 11, this problem has disappeared. All the functionality we need is implemented easily and simply as follows:
Now, in our class, the m_resource field has appeared - this is the same resource that we protect. And now we can initialize it in any way convenient for us. It remains only to realize the possibility of seizing control of a resource and gaining access to it - that is, the most interesting. Let's get started:
So, as we see, we have a new class - SharedResource :: Accessor. This class is the very proxy that provides access to the shared resource while it is being captured. The SharedResource class is declared friendly for it so that this class can call its constructor. The important point is that no one except the parent class can instantiate 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, the mutex is captured, and when 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 unsafe. It's about copying or moving instances of this class. Neither the first nor the second are explicitly declared, which means that default constructors and operators will be used. At the same time, they will not work correctly - for example, when copying, the mutex will not be captured again (which is correct), but will be freed upon destruction. Thus, if an instance of the class is copied, the mutex will be released one more time than it was captured, and we get our favorite Undefined Behavior. Let's try to fix this:
We have forbidden copying, but allowed moving. 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 deadly - displaced objects and 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 a very difficult task in most cases. Nevertheless, it would be nice to give the user the opportunity to check the object for validity. Let's do this by adding this method:
Now the user can always check his copy of the proxy for validity. If desired, you can add an operator bool, although I did not do this. So, it remains to implement only the very access to the shared resource. We do this by adding the following statements for the SharedResource :: Accessor class:
The whole class will look like this:
Done. All basic functionality for this class is implemented and the class is ready for use. Of course, it would be nice to implement an analogue of the Rust method of new_with_condvars, which, when creating a class, associates the mutex with the transferred list of conditional variables (condvars). In C ++, mutexes and conditional variables bind 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 ownership of the mutex without providing access to the resource.
It would be possible to change our implementation so that interaction with condvars became possible, however, I am afraid that in this case the implementation will cease to be simple and reliable, and this was exactly the initial intention. However, those who wish may find below an implementation that works with condvars.
On this, we can consider the implementation of our class complete.
Now a couple of comments on how you can’t use this class so as not to shoot yourself in the foot or something else vital:
Many thanks to all for your attention.
Link to the github code: https://github.com/isapego/shared-resource .
The code is published under the public domain license, so you can do anything with it that only comes to your mind.
I would be glad if the article is useful to someone.
PS
Thanks to everyone who pointed out errors, gave advice here and on the github, and helped make the article and code better.
I often develop programs in C ++ and love this language, no matter what they say. Probably because in many areas he has not yet been replaced. 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 watched with interest the presentation of Stepan Koltsov stepancheg about the Rust programming language, and I really liked the idea of implementing mutexes in this language. And I did not see any obstacles to the implementation of such a primitive in C ++ and immediately opened the IDE, in order to implement this in practice.
I warn you right away that I will write using the C ++ 11 standard, so if you intend to compile the proposed code, you should do this with the flag
-std=c++11
. I also want to immediately warn that I do not pretend to be original and completely admit that such a primitive already exists in some 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 our prototype, works as follows: This is a template class that is parameterized by the type of data that it should protect. That is, in fact, this is not a mutex in the sense that we are all used to, but rather a kind of protected resource that contains not only the mutex itself, but also the value that it protects. Moreover, this type is designed in such a way that access to the protected data was in principle impossible without preliminary capture of the mutex.
Thus, the result should be a template class parameterized by the type that has the properties described above. Let's call it conditionally SharedResource. Work with him should eventually look something like this:
Show code
SharedResource 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 to release it after all operations with the shared resource have been completed. Let's get started. So, let's start with a routine: the stub class itself, which contains standard constructors, destructors, and operators.
Show code
template
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 corny. Copying and moving is prohibited for now, then we will change it if a need or desire arises. But we still haven’t realized the capabilities described even in this line of our usage example:
template
class SharedResource
SharedResource shared_int(5);
We do not have the ability to initialize the resource we protect. Let's try to fix it. When using the standard C ++ 03 or lower, it would be rather problematic (although possible) for the obvious reason - the designers of the protected resource can accept any number of arguments of arbitrary types. However, with the introduction of Variadic Templates in C ++ 11, this problem has disappeared. All the functionality we need is implemented easily and simply as follows:
Show code
template
class SharedResource
{
public:
template
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, in our class, the m_resource field has appeared - this is the same resource that we protect. And now we can initialize it in any way convenient for us. It remains only to realize the possibility of seizing control of a resource and gaining access to it - that is, the most interesting. Let's get started:
Show code
#include
template
class SharedResource
{
public:
template
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;
public:
~Accessor()
{
m_shared_resource.m_mutex.unlock();
}
private:
Accessor(SharedResource &resource) : m_shared_resource(resource)
{
m_shared_resource.m_mutex.lock();
}
SharedResource &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 very proxy that provides access to the shared resource while it is being captured. The SharedResource class is declared friendly for it so that this class can call its constructor. The important point is that no one except the parent class can instantiate 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, the mutex is captured, and when 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 unsafe. It's about copying or moving instances of this class. Neither the first nor the second are explicitly declared, which means that default constructors and operators will be used. At the same time, they will not work correctly - for example, when copying, the mutex will not be captured again (which is correct), but will be freed upon destruction. Thus, if an instance of the class is copied, the mutex will be released one more time than it was captured, and we get our favorite Undefined Behavior. Let's try to fix this:
Show code
#include
template
class SharedResource
{
public:
template
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;
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 *resource) : m_shared_resource(resource)
{
m_shared_resource->m_mutex.lock();
}
SharedResource *m_shared_resource;
};
Accessor lock()
{
return Accessor(this);
}
private:
T m_resource;
std::mutex m_mutex;
};
We have forbidden copying, but allowed moving. 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 deadly - displaced objects and 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 a very difficult task in most cases. Nevertheless, 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 the proxy for validity. If desired, you can add an operator bool, although I did not do this. So, it remains to implement only the very access to the shared resource. We do this by adding the following statements for the SharedResource :: Accessor class:
Show code
T* operator->()
{
return &m_shared_resource->m_resource;
}
T& operator*()
{
return m_shared_resource->m_resource;
}
The whole class will look like this:
Show code
#include
template
class SharedResource
{
public:
template
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;
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 *resource) : m_shared_resource(resource)
{
m_shared_resource->m_mutex.lock();
}
SharedResource *m_shared_resource;
};
Accessor lock()
{
return Accessor(this);
}
private:
T m_resource;
std::mutex m_mutex;
};
Done. All basic functionality for this class is implemented and the class is ready for use. Of course, it would be nice to implement an analogue of the Rust method of new_with_condvars, which, when creating a class, associates the mutex with the transferred list of conditional variables (condvars). In C ++, mutexes and conditional variables bind 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 ownership of the mutex without providing access to the resource.
It would be possible to change our implementation so that interaction with condvars became possible, however, I am afraid that in this case the implementation will cease to be simple and reliable, and this was exactly the initial intention. However, those who wish may find below an implementation that works with condvars.
Show code
#include
template
class SharedResource
{
public:
template
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;
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& get_lock() noexcept
{
return m_lock;
}
private:
Accessor(SharedResource *resource) :
m_lock(resource->m_mutex),
m_shared_resource(&resource->m_resource)
{
}
std::unique_lock 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 complete.
Now a couple of comments on how you can’t use this class so as not to shoot yourself in the foot or something else vital:
- You cannot save links and pointers to the protected resource, so that there is no way to gain access to it bypassing the proxy.
- You cannot call the SharedResource :: lock () method more than once in the same block.
- You cannot use a proxy after moving it.
- Well, if you intend to use an implementation with condvars support, then it is highly recommended that you use unique_lock accessible through the proxy, except for passing the std :: condition_variable class to the wait methods.
Many thanks to all for your attention.
Link to the github code: https://github.com/isapego/shared-resource .
The code is published under the public domain license, so you can do anything with it that only comes to your mind.
I would be glad if the article is useful to someone.
PS
Thanks to everyone who pointed out errors, gave advice here and on the github, and helped make the article and code better.