Параллельное программирование на С++ в действии. Практика разработки многопоточных программ | страница 66
>std::mutex resource_mutex; ←┐
В этой точке все потоки
> │
сериализуются
>void foo() {
> std::unique_lock
> if (!resource_ptr) {
> resource_ptr.reset(new some_resource); ←┐
в защите нуж-
> } │
дается только
> lk.unlock(); │
инициализация
> resource_ptr->do_something();
>}
Этот код встречается настолько часто, а ненужная сериализация вызывает столько проблем, что многие предпринимали попытки найти более приемлемое решение, в том числе печально известный паттерн блокировка с двойной проверкой (Double-Checked Locking): сначала указатель читается без захвата мьютекса (1) (см. код ниже), а захват производится, только если оказалось, что указатель равен >NULL
. Затем, когда мьютекс захвачен (2), указатель проверяется еще раз (отсюда и слова «двойная проверка») на случай, если какой-то другой поток уже выполнил инициализацию в промежутке между первой проверкой и захватом мьютекса:
>void undefined_behaviour_with_double_checked_locking() {
> if (!resource_ptr) ←
(1)
> {
> std::lock_guard
> if (!resource_ptr) ←
(2)
> {
> resource_ptr.reset(new some_resource);←
(3)
> }
> }
> resource_ptr->do_something(); ←
(4)
>}
«Печально известным» я назвал этот паттерн не без причины: он открывает возможность для крайне неприятного состояния гонки, потому что чтение без мьютекса (1) не синхронизировано с записью в другом потоке с уже захваченным мьютексом (3). Таким образом, возникает гонка, угрожающая не самому указателю, а объекту, на который он указывает; даже если один поток видит, что указатель инициализирован другим потоком, он может не увидеть вновь созданного объекта >some_resource
, и, следовательно, вызов >do_something()
(4) будет применен не к тому объекту, что нужно. Такого рода гонка в стандарте С++ называется гонкой за данными (data race), она отнесена к категории неопределенного поведения.
Комитет по стандартизации С++ счел этот случай достаточно важным, поэтому в стандартную библиотеку включен класс >std::once_flag
и шаблон функции >std::call_once
. Вместо того чтобы захватывать мьютекс и явно проверять указатель, каждый поток может просто вызвать функцию >std::call_once
, твердо зная, что к моменту возврата из нее указатель уже инициализирован каким-то потоком (без нарушения синхронизации). Обычно издержки, сопряженные с использованием