Еще один способ справиться с гонками — рассматривать изменения структуры данных как
Самый простой механизм защиты разделяемых данных из описанных в стандарте С++ — это
Итак, у нас есть разделяемая структура данных, например связанный список из предыдущего раздела, и мы хотим защитить его от гонки и нарушения инвариантов, к которым она приводит. Как было бы здорово, если бы мы могли пометить участки кода, в которых производятся обращения к этой структуре данных,
Что ж, это вовсе не сказка — именно такое поведение вы получаете при использовании примитива синхронизации, который называется
Мьютексы — наиболее общий механизм защиты данных в С++, но панацеей они не являются; важно структурировать код так, чтобы защитить нужные данные (см. раздел 3.2.2), и избегать состояний гонки, внутренне присущих интерфейсам (см раздел 3.2.3). С мьютексами связаны и собственные проблемы, а именно:
В С++ для создания мьютекса следует сконструировать объект типа std::mutex
, для захвата мьютекса служит функция-член lock()
, а для освобождения — функция-член unlock()
. Однако вызывать эти функции напрямую не рекомендуется, потому что в этом случае необходимо помнить о вызове unlock()
на каждом пути выхода из функции, в том числе и вследствие исключений. Вместо этого в стандартной библиотеке имеется шаблон класса std::lock_guard
, который реализует идиому RAII — захватывает мьютекс в конструкторе и освобождает в деструкторе, — гарантируя тем самым, что захваченный мьютекс обязательно будет освобожден. В листинге 3.1 показано, как с помощью классов std::mutex
и std::lock_guard
защитить список, к которому могут обращаться несколько потоков. Оба класса определены в заголовке
.
Листинг 3.1. Защита списка с помощью мьютекса
#include
#include
#include
std::list
(1)
std::mutex some_mutex; ←
(2)
void add_to_list(int new_value) {
std::lock_guard
(3)
some_list.push_back(new_value);
}
bool list_contains(int value_to_find) {
std::lock_guard
(4)
return
std::find(some_list.begin(), some_list.end(), value_to_find) !=
some_list.end();
}
В листинге 3.1 есть глобальный список (1), который защищен глобальным же объектом std::mutex
(2). Вызов std::lock_guard
в add_to_list()
(3) и list_contains()
(4) означает, что доступ к списку из этих двух функций является взаимно исключающим: list_contains()
никогда не увидит промежуточного результата модификации списка, выполняемой в add_to_list()
.