#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. Вы правы, мне, вероятно, следует реорганизовать вызывающий код. Спасибо.