Std:: shared_ptr безопасность потоков
Я читал, что
"Несколько потоков могут одновременно читать и писать разные shared_ptr, даже если объекты являются копиями, которые разделяют ( MSDN: безопасность потоков в стандартной библиотеке С++)
Означает ли это, что изменение объекта shared_ptr безопасно?
Для экземпляра следующий код считается безопасным:
shared_ptr<myClass> global = make_shared<myClass>();
...
//In thread 1
shared_ptr<myClass> private = global;
...
//In thread 2
global = make_shared<myClass>();
...
Могу ли я быть уверенным, что в этом случае поток 1 private
будет иметь исходное значение global
или новое значение, которое назначен нить 2, но в любом случае он будет иметь действительный shared_ptr для myClass?
== EDIT ==
Просто объясню свою мотивацию. Я хочу иметь общий указатель для хранения моей конфигурации, и у меня есть пул потоков для обработки запросов.
поэтому global
- глобальная конфигурация.
thread 1
принимает текущую конфигурацию, когда она начинает обрабатывать запрос.
thread 2
обновляет конфигурацию. (применимы только к будущим запросам)
Если он работает, я могу обновить конфигурацию таким образом, не разбивая его в середине обработки запроса.
Ответы
Ответ 1
То, что вы читаете, не означает, что вы думаете, это значит. Прежде всего, попробуйте страницу msdn для самого shared_ptr.
Прокрутите страницу вниз до раздела "Замечания", и вы попадете в суть проблемы. По сути, shared_ptr<>
указывает на "блок управления", который отслеживает, сколько объектов shared_ptr<>
самом деле указывают на "реальный" объект. Итак, когда вы делаете это:
shared_ptr<int> ptr1 = make_shared<int>();
Хотя здесь есть только 1 вызов для выделения памяти через make_shared
, есть два "логических" блока, которые вы не должны обрабатывать одинаково. Одним из них является int
котором хранится фактическое значение, а другим - управляющий блок, в котором хранится вся "магия" shared_ptr<>
которая заставляет его работать.
Только сам блок управления является потокобезопасным.
Я ставлю это на отдельную линию для акцента. Содержимое shared_ptr
не является поточно-ориентированным и не записывает в тот же экземпляр shared_ptr
. Вот что-то, чтобы продемонстрировать, что я имею в виду:
// In main()
shared_ptr<myClass> global_instance = make_shared<myClass>();
// (launch all other threads AFTER global_instance is fully constructed)
//In thread 1
shared_ptr<myClass> local_instance = global_instance;
Это нормально, на самом деле вы можете делать это во всех темах столько, сколько хотите. И затем, когда local_instance
разрушается (выходя из области видимости), он также является потокобезопасным. Кто-то может получить доступ к global_instance
и это не будет иметь значения. Фрагмент, который вы извлекли из msdn, в основном означает "доступ к блоку управления является потокобезопасным", так что другие экземпляры shared_ptr<>
могут создаваться и уничтожаться в разных потоках по мере необходимости.
//In thread 1
local_instance = make_shared<myClass>();
Это отлично. Это повлияет на объект global_instance
, но только косвенно. Блок управления, на который он указывает, будет уменьшен, но выполнен потокобезопасным способом. local_instance
больше не будет указывать на тот же объект (или управляющий блок), что и global_instance
.
//In thread 2
global_instance = make_shared<myClass>();
Это почти наверняка не хорошо, если к global_instance
обращаются из любых других потоков (что вы говорите, что делаете). Требуется блокировка, если вы делаете это, потому что вы пишете туда, где живет global_instance
, а не просто читаете из него. Поэтому запись в объект из нескольких потоков - это плохо, если только вы не защитили его через блокировку. Таким образом, вы можете читать из global_instance
объект, назначая ему новые объекты shared_ptr<>
но вы не можете писать в него.
// In thread 3
*global_instance = 3;
int a = *global_instance;
// In thread 4
*global_instance = 7;
Значение a
не определено. Это может быть 7, или это может быть 3, или это может быть что-нибудь еще. Потоковая безопасность экземпляров shared_ptr<>
применяется только к управлению экземплярами shared_ptr<>
которые были инициализированы друг от друга, а не к тому, на что они указывают.
Чтобы подчеркнуть, что я имею в виду, посмотрите на это:
shared_ptr<int> global_instance = make_shared<int>(0);
void thread_fcn();
int main(int argc, char** argv)
{
thread thread1(thread_fcn);
thread thread2(thread_fcn);
...
thread thread10(thread_fcn);
chrono::milliseconds duration(10000);
this_thread::sleep_for(duration);
return;
}
void thread_fcn()
{
// This is thread-safe and will work fine, though it useless. Many
// short-lived pointers will be created and destroyed.
for(int i = 0; i < 10000; i++)
{
shared_ptr<int> temp = global_instance;
}
// This is not thread-safe. While all the threads are the same, the
// "final" value of this is almost certainly NOT going to be
// number_of_threads*10000 = 100,000. It'll be something else.
for(int i = 0; i < 10000; i++)
{
*global_instance = *global_instance + 1;
}
}
shared_ptr<>
- это механизм, гарантирующий уничтожение объекта несколькими владельцами объектов, а не механизм, гарантирующий, что несколько потоков могут правильно обращаться к объекту. Вам все еще нужен отдельный механизм синхронизации, чтобы безопасно использовать его в нескольких потоках (например, std :: mutex).
Лучший способ думать об этом IMO - это то, что shared_ptr<>
гарантирует, что несколько копий, указывающих на одну и ту же память, не имеют проблем с синхронизацией для себя, но ничего не делает для указанного объекта. Относись к этому так.
Ответ 2
Чтобы добавить к тому, что написал Кевин, спецификация С++ 14 имеет дополнительную поддержку для атомарного доступа к объектам shared_ptr:
20.8.2.6 shared_ptr
атомный доступ [util.smartptr.shared.atomic]
Параллельный доступ к объекту shared_ptr
из нескольких потоков не приводит к гонке данных, если доступ осуществляется исключительно через функции в этом разделе, и экземпляр передается в качестве первого аргумента.
Итак, если вы выполните:
//In thread 1
shared_ptr<myClass> private = atomic_load(&global);
...
//In thread 2
atomic_store(&global, make_shared<myClass>());
...
он будет потокобезопасным.
Ответ 3
Это означает, что у вас будет действительный shared_ptr
и действительный shared_ptr
ссылок.
Вы описываете состояние гонки между двумя потоками, которые пытаются прочитать/назначить одну и ту же переменную.
Поскольку это вообще неопределенное поведение (оно имеет смысл только в контексте и времени отдельной программы), shared_ptr
не справляется с этим.
Ответ 4
Операции чтения не поддаются расстановке данных между собой, поэтому безопасно использовать один и тот же экземпляр shared_ptr между потоками, если все потоки используют только методы const (сюда входят создание его копий). Как только один поток использует метод non-const (как в пункте "point to another object" ), такое использование больше не является потокобезопасным.
Пример OP не является потокобезопасным и потребует использования атомной нагрузки в потоке 1 и атомарного хранилища в потоке 2 (раздел 2.7.2.5 на С++ 11), чтобы сделать его потокобезопасным.
Ключевое слово в тексте MSDN - это действительно разные объекты shared_ptr, как уже указывалось в предыдущих ответах.
Ответ 5
Я думаю, что пока ответы на этот вопрос вводят в заблуждение относительно описанного сценария. У меня очень похожий сценарий, описанный в вопросе. Все остальные потоки имеют (нужен) доступ только для чтения к текущей конфигурации, что достигается с помощью:
// In thread n
shared_ptr<MyConfig> sp_local = sp_global;
Ни один из этих потоков не собирается изменять содержимое объекта MyConfig
. Счетчик sp_global
для sp_global
увеличивается с каждым выполнением строки выше.
sp_global
1 периодически сбрасывает sp_global
к другому экземпляру конфигурации:
// In thread 1
shared_ptr<MyConfig> sp_global = make_shared<MyConfig>(new MyConfig);
Это также должно быть безопасно. Он устанавливает счетчик ссылок sp_global
обратно на 1, и sp_global
теперь указывает на последнюю конфигурацию, как и для всех новых локальных копий. Так что, если я здесь ничего не пропустил, все это должно быть полностью поточно-ориентированным.
#include <iostream>
#include <memory>
using namespace std;
shared_ptr<int> sp1(new int(10));
int main()
{
cout<<"Hello World! \n";
cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
cout << "---------\n";
shared_ptr<int> sp2 = sp1;
shared_ptr<int>* psp3 = new shared_ptr<int>;
*psp3 = sp1;
cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
cout << "sp3 use count: " << psp3->use_count() << ", sp3: " << *(*psp3) << "\n";
cout << "---------\n";
sp1.reset(new int(20));
cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
cout << "sp3 use count: " << psp3->use_count() << ", sp3: " << *(*psp3) << "\n";
cout << "---------\n";
delete psp3;
cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
cout << "---------\n";
sp1 = nullptr;
cout << "sp1 use count: " << sp1.use_count() << "\n";
cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
return 0;
}
и выход
Hello World!
sp1 use count: 1, sp1: 10
---------
sp1 use count: 3, sp1: 10
sp2 use count: 3, sp2: 10
sp3 use count: 3, sp3: 10
---------
sp1 use count: 1, sp1: 20
sp2 use count: 2, sp2: 10
sp3 use count: 2, sp3: 10
---------
sp1 use count: 1, sp1: 20
sp2 use count: 1, sp2: 10
---------
sp1 use count: 0
sp2 use count: 1, sp2: 10