Подтвердить что ты не робот

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 обновляет конфигурацию. (применимы только к будущим запросам)

Если он работает, я могу обновить конфигурацию таким образом, не разбивая его в середине обработки запроса.

4b9b3361

Ответ 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