Иногда нам не нужны издержки, которыми сопровождается перенос зависимости. Мы хотим, чтобы компилятор мог кэшировать значения в регистрах и изменять порядок операций во имя оптимизации кода, а не волновался по поводу зависимостей. В таких случаях можно воспользоваться шаблоном функции std::kill_dependency() для явного разрыва цепочки зависимостей. Эта функция просто копирует переданный ей аргумент в возвращаемое значение, но попутно разрывает цепочку зависимостей. Например, если имеется глобальный массив с доступом только для чтения, и вы используете семантику std::memory_order_consume при чтении какого-то элемента этого массива из другого потока, то с помощью std::kill_dependency() можно сообщить компилятору, что ему необязательно заново считывать содержимое элемента массива (см. пример ниже).

int global_data[] = { ... };

std::atomic index;

void f() {

 int i = index.load(std::memory_order_consume);

 do_something_with(global_data[std::kill_dependency(i)]);

}

Разумеется, в таком простом случае вы вряд ли вообще будете пользоваться семантикой std::memory_order_consume, но в аналогичной ситуации функцией std::kill_dependency() можно воспользоваться и в более сложной программе. Только не забывайте, что это оптимизация, поэтому прибегать к ней следует с осторожностью и только тогда, когда профилирование ясно продемонстрировало необходимость.

Теперь, рассмотрев основы упорядочения доступа к памяти, мы можем перейти к более сложным аспектам отношения синхронизируется-с, которые проявляются в форме последовательностей освобождений (release sequences).

<p>5.3.4. Последовательности освобождений и отношение синхронизируется-с</p>

В разделе 5.3.1 я упоминал, что можно получить отношение синхронизируется-с между операцией сохранения атомарной переменной и операцией загрузки той же атомарной переменной в другом потоке, даже если между ними выполняется последовательность операций чтения-модификации-записи, — при условии, что все операции помечены надлежащим признаками. Теперь, когда мы знаем обо всех возможных «признаках» упорядочения, я могу подробнее осветить этот вопрос. Если операция сохранения помечена одним из признаков memory_order_release, memory_order_acq_rel или memory_order_seq_cst, а операция загрузки — одним из признаков memory_order_consume, memory_order_acquire или memory_order_seq_cst, и каждая операция в цепочке загружает значение, записанное предыдущей операцией, то такая цепочка операций составляет последовательность освобождений, и первая в ней операция сохранения синхронизируется-с (в случае memory_order_acquire или memory_order_seq_cst) или предшествует-по-зависимости (в случае memory_order_consume) последней операции загрузки. Любая атомарная операция чтения-модификации-записи в цепочке может быть помечена произвольным признаком упорядочения (даже memory_order_relaxed).

Чтобы попять, что это означает и почему так важно, рассмотрим значение типа atomic, которое используется как счетчик count элементов в разделяемой очереди (см. листинг ниже).

Листинг 5.11. Чтение из очереди с применением атомарных операций

#include

#include

std::vector queue_data; std::atomic count;

void populate_queue() {

 unsigned const number_of_items = 20;

 queue_data.clear();

 for (unsigned i = 0; i < number_of_items; ++i) {

  queue_data.push_back(i);

 } ←(1) Начальное сохранение

 count.store(number_of_items, std::memory_order_release);

}

void consume_queue_items() {

 while (true) { ←(2) Операция ЧМЗ

 int item_index;

 if (

  (item_index =

   count.fetch_sub(1, std::memory_order_acquire)) <= 0) {

   wait_for_more_items();←┐Ждем дополнительных

   continue;             (3) элементов

 }

 process(queue_data[item_index-1]);←┐Чтение из queue_data

}                                  (4) безопасно

int main() {

 std::thread a(populate_queue);

 std::thread b(consume_queue_items);

Перейти на страницу:

Похожие книги