Использование ограждения памяти C11

Даже для простого примера связи с двумя потоками я с трудом могу выразить это в стиле атома C11 и memory_fence, чтобы получить правильное упорядочение памяти:

общие данные:

volatile int flag, bucket;

поток производителя:

while (true) {
   int value = producer_work();
   while (atomic_load_explicit(&flag, memory_order_acquire))
      ; // busy wait
   bucket = value;
   atomic_store_explicit(&flag, 1, memory_order_release);
}

потребительский поток:

while (true) {
   while (!atomic_load_explicit(&flag, memory_order_acquire))
      ; // busy wait
   int data = bucket;
   atomic_thread_fence(/* memory_order ??? */);
   atomic_store_explicit(&flag, 0, memory_order_release);
   consumer_work(data);
}

Насколько я понимаю, выше код будет правильно заказывать хранилище-в-ковке → флаг-store- > flag-load → load-from-bucket. Тем не менее, я думаю, что между нагрузкой и ведром остается состояние гонки и снова записывать ведро с новыми данными. Чтобы заставить порядок следовать за чтением в bucket, я думаю, мне понадобится явный atomic_thread_fence() между прочитанным ведром и следующим атомарным_стопом. К сожалению, не существует аргумента memory_order для обеспечения чего-либо в предыдущих нагрузках, даже memory_order_seq_cst.

Действительно грязное решение может состоять в том, чтобы повторно назначить bucket в потребительском потоке с фиктивным значением: это противоречит понятию потребительского чтения.

В старом мире C99/GCC я мог бы использовать традиционный __sync_synchronize(), который, я считаю, был бы достаточно сильным.

Каким было бы более удобное решение C11 для синхронизации этой так называемой антизависимости?

(Конечно, я знаю, что лучше избегать такого низкоуровневого кодирования и использовать доступные конструкторы более высокого уровня, но я хотел бы понять...)

Ответ 1

Чтобы заставить порядок следовать за чтением в bucket, я думаю, мне понадобится явный atom_thread_fence() между прочитанным ведром и следующим атомарным_стопом.

Я не считаю, что вызов atomic_thread_fence() необходим: обновление флага имеет семантику выпуска, что предотвращает переупорядочение любых предыдущих операций загрузки или хранения. См. Формальное определение Херба Саттера:

Списание записи выполняется после чтения и записи тем же потоком, который предшествует ему в порядке выполнения.

Это должно препятствовать переупорядочению чтения bucket после обновления flag независимо от того, где компилятор хочет сохранить data.

Это приводит меня к вашему комментарию о другом ответе:

volatile гарантирует, что генерируются ld/st операции, которые впоследствии могут быть заказаны с помощью ограждений. Однако данные являются локальной переменной, а не изменчивой. Компилятор, вероятно, поместит его в регистр, избегая операции хранилища. Это позволяет загрузить нагрузку с ведра с последующим флагом reset.

Казалось бы, это не проблема, если чтение bucket не может быть переупорядочено за flag write-release, поэтому volatile не требуется (хотя, вероятно, это не помешает иметь его, либо). Это также необязательно, поскольку большинство вызовов функций (в данном случае atomic_store_explicit(&flag)) служат в качестве барьеров памяти времени компиляции. Компилятор не изменил порядок чтения глобальной переменной после вызова неинтегрированной функции, поскольку эта функция может изменять одну и ту же переменную.

Я также согласен с @MaximYegorushkin в том, что вы можете улучшить ожидание с помощью команд pause при настройке совместимых архитектур. Кажется, что GCC и ICC имеют _mm_pause(void) intrinsics (вероятно, эквивалентны __asm__ ("pause;")).

Ответ 2

Я согласен с тем, что @MikeStrobel говорит в своем комментарии.

Здесь вам не нужно atomic_thread_fence(), потому что ваши критические разделы начинаются с приобретения и заканчиваются семантикой выпуска. Следовательно, чтение в ваших критических разделах не может быть переупорядочено до получения и записи после выпуска. И вот почему volatile здесь тоже не нужно.

Кроме того, я не вижу причины, по которой вместо этого здесь не используется спин-блокировка (pthread). spinlock делает для вас одинаковое занятие, но также использует pause инструкция:

Внутренняя пауза используется в циклах ожидания ожидания с процессорами, реализующими динамическое выполнение (особенно выполнение вне порядка). В цикле спина-ожидания внутренняя пауза улучшает скорость, с которой код обнаруживает освобождение блокировки и обеспечивает особенно значительное увеличение производительности. Выполнение следующей команды задерживается на определенное время выполнения. Инструкция PAUSE не изменяет архитектурное состояние. Для динамического планирования команда PAUSE уменьшает штраф за выход из цикла вращения.

Ответ 3

Прямой ответ:

То, что ваш магазин является операцией memory_order_release, означает, что ваш компилятор должен выдать забор памяти для инструкции хранилища перед хранилищем флага. Это необходимо для обеспечения того, чтобы другие процессоры отображали конечное состояние выпущенных данных до того, как они начали его интерпретировать. Итак, нет, вам не нужно добавлять второй забор.


Длинный ответ:

Как отмечалось выше, происходит то, что компилятор преобразует ваши инструкции atomic_... в комбинации забора и доступа к памяти; фундаментальная абстракция - это не атомная нагрузка, а забор памяти. Вот как все работает, хотя новые абстракции С++ заставляют вас думать по-другому. И я лично считаю, что заботы о памяти гораздо легче думать, чем проворные абстракции в С++.

С точки зрения аппаратного обеспечения необходимо обеспечить относительный порядок загрузки и хранения, т.е. е. что запись в bucket завершается до того, как флаг будет записан в производителе, и что нагрузка флага считывает значение, превышающее нагрузку на ведро пользователя.

Тем не менее, вам действительно нужно:

//producer
while(true) {
    int value = producer_work();
    while (flag) ; // busy wait
    atomic_thread_fence(memory_order_acquire);  //ensure that value is not assigned to bucket before the flag is lowered
    bucket = value;
    atomic_thread_fence(memory_order_release);  //ensure bucket is written before flag is
    flag = true;
}

//consumer
while(true) {
    while(!flag) ; // busy wait
    atomic_thread_fence(memory_order_acquire);  //ensure the value read from bucket is not older than the last value read from flag
    int data = bucket;
    atomic_thread_fence(memory_order_release);  //ensure data is loaded from bucket before the flag is lowered again
    flag = false;
    consumer_work(data);
}

Обратите внимание, что этикетки "производитель" и "потребитель" вводят в заблуждение здесь, потому что у нас есть два процесса, играющих в пинг-понг, каждый из которых становится производителем и потребителем по очереди; это просто, что один поток создает полезные значения, а другой создает "дыры" для ввода полезных значений в...

atomic_thread_fence() - это все, что вам нужно, и поскольку он непосредственно переводится в инструкции ассемблера ниже абстракций atomic_..., это гарантированно будет самым быстрым подходом.