std :: shared_ptr в одном писателе дизайн многих читателей является потокобезопасным? - PullRequest
2 голосов
/ 15 апреля 2019

На многопоточном сервере один поток (записывающее устройство) периодически обновляет данные из базы данных, а другой поток (считывающее устройство) обрабатывает запрос пользователя с этими данными.

Я пытаюсь использовать блокировку чтения / записи для удовлетворения этого требования.запрос, но производительность настолько плохая, что нужно найти что-то еще.

Я читаю из https://en.cppreference.com/w/cpp/memory/shared_ptr, он говорит:

Все функции-члены (включая конструктор копирования и копированияприсваивание) может вызываться несколькими потоками в разных экземплярах shared_ptr без дополнительной синхронизации , даже если эти экземпляры являются копиями и имеют общее владение одним и тем же объектом .

Затем после некоторого исследования я использую дляstd :: shared_ptr, чтобы сделать это.Код выглядит примерно так:

// this class is singleton
class DataManager{
public:
    // all the reader thread use this method to get data and release shared_ptr 
    // at the end of user's request
    std::shared_ptr<Data> get_main_ptr(){
        return _main_data;
    }
private:
    // data1
    std::shared_ptr<Data> _main_data;
    // data2
    std::shared_ptr<Data> _back_data;

    // read database, write data in to _data
    void update_data(std::shared_ptr<Data> _data);

    // this function called at a separate thread every 10 min
    bool reload_data(){
        // write data in back pointer
        update_data(_back_data);

        //save the _main_data
        std::shared_ptr<Data> old_ptr = _main_data;

        //exchange pointer, reader thread hold the copy of _main_data
        _main_data = _back_data;

        // wait until reader threads release all copy of _main_data
        while(old_ptr.use_count() != 1) {
            sleep(5);
        }

        // clear the data
        old_ptr->clear();
        _back_data = old_ptr;
        return;
}

}

Этот метод работает в рабочей среде.но я не совсем уверен и не понимаю потокобезопасный уровень shared_ptr.Есть ли проблема в этом методе?Или другие предложения, чтобы удовлетворить мою просьбу

Ответы [ 2 ]

3 голосов
/ 15 апреля 2019

Похоже, что вы переназначаете shared_ptr, который разделяется между потоками:

_main_data = _back_data;

Если другой поток читает или копирует _main_data одновременно, он может получить поврежденную копию.

Назначение shared_ptr не является потокобезопасным, поскольку shared_ptr содержит два члена-указателя, и они не могут быть обновлены атомарно. См shared_ptr:

Если несколько потоков выполнения обращаются к одному и тому же shared_ptr без синхронизации и любой из этих обращений использует неконстантную функцию-член shared_ptr, тогда произойдет гонка данных;

Чтобы исправить это состояние гонки, код должен использовать atomic_store:

atomic_store(&_main_data, _back_data);

И читатели должны сделать:

auto main_data = atomic_load(&_main_data);

Раздел примечаний полезно:

Эти функции обычно реализуются с использованием мьютексов, которые хранятся в глобальной хеш-таблице, где в качестве ключа используется значение указателя.

Чтобы избежать гонок данных, после того, как общий указатель передан какой-либо из этих функций, к нему нельзя получить доступ не атомарно. В частности, вы не можете разыменовать такой shared_ptr без предварительной атомной загрузки его в другой объект shared_ptr, а затем разыменования через второй объект.

Concurrency TS предлагает атомные классы интеллектуальных указателей atomic_shared_ptr и atomic_weak_ptr в качестве замены для использования этих функций.

Начиная с C ++ 20: Эти функции устарели в пользу специализаций шаблона std::atomic: std::atomic<std::shared_ptr> и std::atomic<std::weak_ptr>.


Кроме того, вы должны заставить Data деструктор выполнять всю очистку, чтобы вам не приходилось ждать, пока потоки считывателя не выпустят _main_data, чтобы очистить его вручную.


Кроме того, вы можете использовать std::atomic и boost::intrusive_ptr, чтобы сделать обновление указателя данных поточно-безопасным, атомарным, без ожидания и без утечек.

Преимущество использования boost::intrusive_ptr вместо std::shared_ptr состоит в том, что первое может быть безопасно создано потоком из простого указателя, поскольку атомный счетчик ссылок хранится внутри объекта.

Рабочий пример:

#include <iostream>
#include <atomic>

#include <boost/smart_ptr/intrusive_ptr.hpp>
#include <boost/smart_ptr/intrusive_ref_counter.hpp>

struct Data
    : boost::intrusive_ref_counter<Data, boost::thread_safe_counter>
{};

using DataPtr = boost::intrusive_ptr<Data>;

class DataAccessor
{
    std::atomic<Data*> data_ = 0;

public:
    ~DataAccessor() {
        DataPtr{data_.load(std::memory_order_acquire), false}; // Destroy data_.
    }

    DataPtr get_data() const {
        return DataPtr{data_.load(std::memory_order_acquire)};
    };

    void set_data(DataPtr new_data) {
        DataPtr old_data{data_.load(std::memory_order_relaxed), false}; // Destroy data_.
        data_.store(new_data.detach(), std::memory_order_release);
    }
};

int main() {
    DataAccessor da;

    DataPtr new_data{new Data};
    da.set_data(new_data);
    DataPtr old_data = da.get_data();
    std::cout << (new_data == old_data) << '\n';
}

valgrind пробег:

$ valgrind ./test
==21502== Memcheck, a memory error detector
==21502== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==21502== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==21502== Command: ./test
==21502== 
1
==21502== 
==21502== HEAP SUMMARY:
==21502==     in use at exit: 0 bytes in 0 blocks
==21502==   total heap usage: 4 allocs, 4 frees, 73,736 bytes allocated
==21502== 
==21502== All heap blocks were freed -- no leaks are possible
==21502== 
==21502== For counts of detected and suppressed errors, rerun with: -v
==21502== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
1 голос
/ 15 апреля 2019

Вы пропустили эту часть:

Если несколько потоков выполнения обращаются к одному shared_ptr без синхронизация и любой из этих доступов использует неконстантный член функция shared_ptr, тогда гонка данных произойдет

shared_ptr - это просто контейнер для вашего указателя. Это не потокобезопасно. Вы можете сделать это, но проще использовать замки.

...