Потокобезопасность контейнера стандартной библиотеки C++ по отношению к содержащимся объектам

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

Проще говоря: если вы заблокируете, скажем, карту для чтения, можете ли вы безопасно (насколько это касается карты) изменять содержащиеся объекты (в данном случае значение), если вы не вставляете или не удаляете элементы или иным образом вызывать неконстантные методы на карте?


person nccc    schedule 17.01.2014    source источник


Ответы (3)


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

Нет, не "независимо". Учитывая «одновременные потоки [доступ] const членов» [контейнера], они могут получить постоянный доступ к сохраненным элементам, но контейнер не позволяет делать с объектами что-либо, что было бы недопустимым, если бы объекты были например локальные переменные - т. е. вы не можете вызывать методы, которые влияют на изменяемые или статические переменные, небезопасным для потоков способом.

Проще говоря: если вы заблокируете, скажем, карту для чтения, можете ли вы безопасно (насколько это касается карты) изменять содержащиеся объекты (в данном случае значение), если вы не вставляете или не удаляете элементы или иным образом вызывать неконстантные методы на карте?

Если под «заблокировать карту для чтения» вы подразумеваете, что ваша программа имеет отдельную блокировку чтения/записи и получает состояние блокировки «читатель» перед доступом к карте, тогда нет — вы не можете изменять содержащиеся объекты, если другие читатели могут получить доступ их. Чтобы сделать это безопасным, вам нужен мьютекс вокруг использования карты, как если бы потоки работали с локальной переменной.


Примеры

Ниже пустая строка разделяет примеры, а в первом и втором столбцах перечислены команды из двух потоков, которые могут выполняться в любом порядке или одновременно. Обратите внимание, что только потому, что что-то «безопасно» для выполнения, не означает, что обновление будет видно в других потоках, пока не будет выполнен какой-либо явный барьер памяти или операция очистки кеша - это зависит от вашего оборудования: «правильный» мьютекс / rwlocks и т. д. склонны заботиться об этом.

class X { int n_; std::string s_; } x;

std::vector<X> v = ...;
std::map<int,X> m = ...;

thread 1                thread 2                  safe?

v.push_back(...);       ++v[0].n_;                Precondition: 1 <= size() < capacity()
                                                  (i.e. safe iff v[0] can't be moved)

v.some-const-member();  v.another-const-member(); YES - e.g. [n], find(), begin()

v[0].s_ = "hi";         std::cout << v[0].s_;     NO - as for any string var

v[0].s_.size();         std::cout << v[0].s_;     YES - as for any string var

rw_lock.r_lock() LOCKED
iterator i = m.find(7); rw_lock.w_lock() BLOCK
rw_lock.r_unlock()      ....
                        LOCKED
std::cout << i->second; m.insert(...);            YES - insert can't invalidate i
i->second.n_ += 3;      m.find(7).n_ -= 3;        NO - as per any int var
person Tony Delroy    schedule 17.01.2014
comment
Спасибо за ваши подробные примеры. Мой вопрос на самом деле близок к вашему последнему. Независимо от безопасности n_ += 3 или n_ -= 3 (содержащегося объекта), m (контейнера) по-прежнему обеспечивает те же гарантии, верно? - person nccc; 17.01.2014
comment
Хорошо, давайте сделаем это более явным. Ниже безопасно? std::map<int, pthread_mutex_t> m2; void my_thread(int x) { rw_lock.r_lock(); pthread_mutex_lock(m2.find(x).second); rw_lock.r_unlock(); } - person nccc; 17.01.2014
comment
@nccc: да, это безопасно (предполагая, очевидно, что другие потоки также используют rw_lock вокруг своих обращений, и найдено x, и игнорируя, что это должно быть find(x)->... ;-)). С map вещи, которые не были бы безопасными без rw_lock, включают, например. .clear() и .erase на этом конкретном узле, а также вызов my_thread(x) для x, который все еще вставляется, поскольку узлы дерева могут быть не полностью связаны и/или сам мьютекс может быть неправильно инициализирован, что делает вызов pthread_mutex_lock() опасным. .. - person Tony Delroy; 17.01.2014

Я думаю, что на этот вопрос уже можно ответить в контейнерах C++11 STL и безопасности потоков. Пока вы можете гарантировать, что ваши потоки никогда не будут обращаться к одним и тем же элементам друг друга, они могут делать с ними все, что им заблагорассудится. Сами контейнеры не имеют никаких гарантий потокобезопасности.

person hvanbrug    schedule 17.01.2014

Контейнеры STL не гарантируют потокобезопасность.

одновременное чтение в порядке (вы не меняете элементы в контейнере или какое-либо свойство контейнера).

также обратите внимание на проверку итератора, определенная функция делает недействительным часть или все итераторы. (вы можете найти информацию из cplusplus или cppreference)

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

person wacky6    schedule 17.01.2014