Правильный способ создания thread_safe shared_ptr без блокировки?

#c #thread-safety #shared-ptr #atomic

#c #безопасность потоков #shared-ptr #атомарный

Вопрос:

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

По сути, если указатель равен нулю, выигрывает первый поток, который задает его значение, а все остальные потоки, которые создают его одновременно, используют значение победившего потока.

Вот что у меня есть на данный момент (обратите внимание, что единственной рассматриваемой функцией является функция CreateIfNotExist(), остальное предназначено для тестирования):

 #include <memory>
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>

struct A {
    A(int a) : x(a) {}
    int x;
};

struct B {
    B() : test(nullptr) {}

    void CreateIfNotExist(int val) {
        std::shared_ptr<A> newPtr = std::make_shared<A>(val);
        std::shared_ptr<A> _null = nullptr;
        std::atomic_compare_exchange_strong(amp;test, amp;_null, newPtr);
    }

    std::shared_ptr<A> test;
};

int gRet = -1;
std::mutex m;

void Func(B* b, int val) {
    b->CreateIfNotExist(val);
    int ret =  b->test->x;

    if(gRet == -1) {
        std::unique_lock<std::mutex> l(m);
        if(gRet == -1) {
            gRet = ret;
        }
    }

    if(ret != gRet) {
        std::cout << " FAILED " << std::endl;
    }
}

int main() {
    B b;

    std::vector<std::thread> threads;
    for(int i = 0; i < 10000;   i) {
        threads.clear();
        for(int i = 0; i < 8;   i) threads.emplace_back(amp;Func, amp;b, i);
        for(int i = 0; i < 8;   i) threads[i].join();
    }
}
  

Это правильный способ сделать это? Есть ли лучший способ гарантировать, что все потоки, вызывающие CreateIfNotExist() одновременно, используют один и тот же shared_ptr?

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

1. Почему бы не сделать B not default сконструируемым, поэтому test он должен быть действительным?

2. Просто определите один общий указатель и передавайте его каждому потоку по мере его создания?

3. @sji Я намеренно структурировал код таким образом, чтобы он соответствовал моему варианту использования, это будет невозможно без большого рефакторинга

4. @Andrew достаточно справедливо.

Ответ №1:

Возможно, что-то в этом роде:

 struct B {
  void CreateIfNotExist(int val) {
    std::call_once(test_init,
                   [this, val](){test = std::make_shared<A>(val);});
  }

  std::shared_ptr<A> test;
  std::once_flag test_init;
};
  

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

1. Насколько легким является этот подход? Будет ли сравнение и обмен более эффективным?

2. Это зависит от вашего варианта использования. Вы увеличиваете структуру для once_flag , но гарантируете, что будет создана только одна структура. С другой стороны, подход, основанный на вопросе, не будет раздувать структуру, но может иметь n почти одновременные конструкции A , только чтобы отбросить все, кроме одного из них. Что лучше для вас, зависит от вас.

3. Предположим, у меня также есть требование, чтобы я мог установить shared_ptr в nullptr в любой момент, будет ли этот подход работать?

4. Что ожидается, если один поток вызывает ResetTestToNull , а другой вызывает CreateIfNotExist примерно в одно и то же время? Вы хотите сказать, что тот, кто вызывает CreateIfNotExist , не может просто продолжить и получить доступ test впоследствии (как это делает ваш пример кода), поскольку какой-то другой поток мог сбросить его сразу после CreateIfNotExist возврата вызова? Я не уверен, что вполне понимаю, как можно написать что-либо похожее на надежный код при таких предположениях.

5. Вы правы, мне, вероятно, следует реорганизовать вызывающий код. Спасибо.