Этот код встречается настолько часто, а ненужная сериализация вызывает столько проблем, что многие предпринимали попытки найти более приемлемое решение, в том числе печально известный паттерн 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) будет применен не к тому объекту, что нужно. Такого рода гонка в стандарте С++ называется
Комитет по стандартизации С++ счел этот случай достаточно важным, поэтому в стандартную библиотеку включен класс std::once_flag
и шаблон функции std::call_once
. Вместо того чтобы захватывать мьютекс и явно проверять указатель, каждый поток может просто вызвать функцию std::call_once
, твердо зная, что к моменту возврата из нее указатель уже инициализирован каким-то потоком (без нарушения синхронизации). Обычно издержки, сопряженные с использованием std::call_once
, ниже, чем при явном применении мьютекса, поэтому такое решение следует предпочесть во всех случаях, когда оно не противоречит требованиям задачи. В примере ниже код из листинга 3.11 переписан с использованием std::call_once
. В данном случае инициализация производится путем вызова функции, но ничто не мешает завести для той же цели класс, в котором определен оператор вызова. Как и большинство функций в стандартной библиотеке, принимающих в качестве аргументов функции или предикаты, std::call_once
работает как с функциями, так и с объектами, допускающими вызов.
std::shared_ptr
std::once_flag resource_flag;←
(1)
void init_resource() {
resource_ptr.reset(new some_resource);
}
│
Инициализация производится
void foo() { ←┘
ровно один раз
std::call_once(resource_flag, init_resource);
resource_ptr->do_something();
}
Здесь переменная типа std::once_flag
(1) и инициализируемый объект определены в области видимости пространства имен, но std::call_once()
вполне можно использовать и для отложенной инициализации членов класса, как показано в следующем листинге.
Листинг 3.12. Потокобезопасная отложенная инициализация члена класса с помощью функции std::call_once()
class X {
private:
connection_infо connection_details;
connection_handle connection;
std::once_flag connection_init_flag;
void open_connection() {
connection = connection_manager.open(connection_details);
}
public:
X(connection_info const& connection_details_):
connection_details(connection_details_) {}
void send_data(data_packet const& data)←
(1)
{
std::call_once(
connection_init_flag, &X::open_connection, this);←┐
connection.send_data(data); │
} │