c++ – Thread-safe wrapper over an object

Question:

Are there any downsides that may force you not to use such wrappers.
Also tell me if there is already something similar in stl or boost. Here is an example:

Safe Ref:

#include <vector>
#include <iostream>
#include <mutex>

template<typename RefType>
class SafeRef
{
public:

    SafeRef(std::mutex &mutex, RefType ref) : m_reference(ref), m_mutex(mutex)
    {
        m_mutex.lock();
    }
    SafeRef(SafeRef &&other)
        : m_mutex(other.m_mutex), m_reference(other.m_referance)
    {
    }

    RefType get() const { return m_reference; }

    ~SafeRef() { m_mutex.unlock(); }
private:
    std::mutex &m_mutex;
    RefType m_reference;
};

Test code:

class SomeObj
{
public:
    SafeRef<const std::vector<int>&> getVector() const
    {
        return SafeRef<const std::vector<int>&>(m_mutex, m_vector);
    }
    void reserve(int n)
    {
        std::lock_guard<std::mutex>(this->m_mutex);
        m_vector.reserve(n);
    }
    void addElement(int i)
    {
        std::lock_guard<std::mutex>(this->m_mutex);
        m_vector.push_back(i);
    }
private:
    mutable std::mutex m_mutex;
    std::vector<int> m_vector; //Большой вектор, копирование очень затратно
};

void write(SomeObj &obj)
{
    obj.reserve(500);
    for (int i = 0; i < 500; i++)
    {
        obj.addElement(i);
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

void read(const SomeObj &obj)
{
    for (int i = 0; i < 5; i++)
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        auto v = obj.getVector();
        std::cout << v.get().size() << std::endl;
    }
}

int main()
{
    SomeObj obj;
    std::thread thread1(&write, std::ref(obj));
    std::thread thread2(&read, std::ref(obj));
    thread2.join();
    thread1.join();

    return 0;
}

Answer:

The problem with this approach is that thread safety is much more complicated .

You can't just make an object thread-safe by protecting all operations with a mutex. For example, you have a wrapper over the stack, let's look at the code then

SupposedlySafeStack<int> s;
// ...
if (!s.is_empty())
    s.pop();

The problem here is that between if (!s.is_empty()) and s.pop(); the stack could very well be empty.

A good thread-safe stack requires a different set of operations. For example, atomic bool try_pop(T&) and push_range(Iterator begin, Iterator end) . Thread safety should be planned at the level of the external interface of the class.

The same applies to index access to std::vector and so on. Therefore, wrappers that wrap all public functions are unsuitable: they only create a false sense of security!

A wrapper for the whole object already exists, it's std::lock_guard .


Also, you usually need to make not just one object thread-safe, but a group of objects and their relationships. For example, you have a queue and a stack, as in the marshalling yard algorithm ; it makes no sense to have a separate protected stack and a separate queue, because in this case they can disagree during your operation! For example, if you push all elements from the stack to the queue, then after removing all the elements from the stack before adding to the queue, new elements may appear on the stack, and the invariant of your algorithm will be violated.

You will have to create an explicit mutex, and protect not objects, but your operations with objects .

Scroll to Top