Параллельное программирование на С++ в действии. Практика разработки многопоточных программ | страница 66



>std::mutex resource_mutex; ←┐В этой точке все потоки

>                            │сериализуются

>void foo() {

> std::unique_lock lk(resource_mutex);

> 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 lk(resource_mutex);

>  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, твердо зная, что к моменту возврата из нее указатель уже инициализирован каким-то потоком (без нарушения синхронизации). Обычно издержки, сопряженные с использованием