Страницы

Поиск по вопросам

вторник, 31 декабря 2019 г.

Потокобезопасная обертка над объектом

#cpp #многопоточность #cpp11 #инспекция_кода


Есть ли минусы, которые могут заставить не использовать подобные обертки.
Также подскажите, есть ли уже что-то подобное в stl или boost.
Вот накидал пример:

SafeRef:

#include 
#include 
#include 

template
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;
};


Тестовый код:

class SomeObj
{
public:
    SafeRef&> getVector() const
    {
        return SafeRef&>(m_mutex, m_vector);
    }
    void reserve(int n)
    {
        std::lock_guard(this->m_mutex);
        m_vector.reserve(n);
    }
    void addElement(int i)
    {
        std::lock_guard(this->m_mutex);
        m_vector.push_back(i);
    }
private:
    mutable std::mutex m_mutex;
    std::vector 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;
}

    


Ответы

Ответ 1



Проблема этого подхода в том, что потокобезопасность намного сложнее. Вы не можете просто так сделать объект потокобезопасным, защитив все операции мьютексом. К примеру, у вас есть обёртка над стеком, дававйте посмотрим тогда на код SupposedlySafeStack s; // ... if (!s.is_empty()) s.pop(); Проблема здесь в том, что между выполнением if (!s.is_empty()) и s.pop(); стек вполне мог стать пустым. Для хорошего потокобезопасного стека нужен другой набор операций. Например, атомарный bool try_pop(T&) и push_range(Iterator begin, Iterator end). Потокобезопасность нужно планировать на уровне внешнего интерфейса класса. То же относится к доступу по индексу к std::vector и так далее. Поэтому обёртки, которые оборачивают все публичные функции, непригодны: они лишь создают ложное чувство безопасности! А обёртка на целый объект уже существует, это std::lock_guard. Кроме того, обычно вам нужно сделать потокобезопасным не один объект, а группу объектов и их отношения между собой. Например, у вас есть очередь и стек, как в алгоритме сортировочной станции; нет смысла иметь отдельно защищённый стек и отдельно очередь, потому что при этом они могут рассогласоваться во время вашей операции! Например, если вы переносите все элементы из стека в очередь, то после удаления всех элементов из стека перед добавлением в очередь в стеке могут образоваться новые элементы, и инвариант вашего алгоритма нарушится. Вам придётся заводить явный мьютекс, и защищать не объекты, а ваши операции с объектами.

Комментариев нет:

Отправить комментарий