многопоточность — атомарность C ++ и сквозная видимость

AFAIK C ++ атомика (<atomic>) семья предоставляет 3 пособия:

  • неделимость примитивных инструкций (нет грязных чтений),
  • упорядочение памяти (как для процессора, так и для компилятора) и
  • поперечная видимость / изменения распространения.

И я не уверен насчет третьего пункта, поэтому взглянем на следующий пример.

#include <atomic>

std::atomic_bool a_flag = ATOMIC_VAR_INIT(false);
struct Data {
int x;
long long y;
char const* z;
} data;

void thread0()
{
// due to "release" the data will be written to memory
// exactly in the following order: x -> y -> z
data.x = 1;
data.y = 100;
data.z = "foo";
// there can be an arbitrary delay between the write
// to any of the members and it's visibility in other
// threads (which don't synchronize explicitly)

// atomic_bool guarantees that the write to the "a_flag"// will be clean, thus no other thread will ever read some
// strange mixture of 4bit + 4bits
a_flag.store(true, std::memory_order_release);
}

void thread1()
{
while (a_flag.load(std::memory_order_acquire) == false) {};
// "acquire" on a "released" atomic guarantees that all the writes from
// thread0 (thus data members modification) will be visible here
}

void thread2()
{
while (data.y != 100) {};
// not "acquiring" the "a_flag" doesn't guarantee that will see all the
// memory writes, but when I see the z == 100 I know I can assume that
// prior writes have been done due to "release ordering" => assert(x == 1)
}

int main()
{
thread0(); // concurrently
thread1(); // concurrently
thread2(); // concurrently

// join

return 0;
}

Во-первых, пожалуйста, подтвердите мои предположения в коде (особенно thread2).

Во-вторых, мои вопросы:

  1. Как работает a_flag писать распространять на другие ядра?

  2. Ли std::atomic синхронизировать a_flag в кеше писателя с кешем других ядер (используя MESI или что-то еще), или распространение происходит автоматически?

  3. Предполагая, что на конкретной машине запись во флаг является атомарной (например, int_32 на x86) И у нас нет никакой личной памяти для синхронизации (у нас есть только флаг), нам нужно использовать атомику?

  4. Принимая во внимание наиболее популярные архитектуры ЦП (x86, x64, ARM v.whither, IA-64), это межъядерная видимость (я сейчас не учитывая переупорядочения) автоматически (но потенциально с задержкой), или вам нужно вводить конкретные команды для распространения любого фрагмента данных?

6

Решение

  1. Сами ядра не имеют значения. Вопрос «как все ядра видят одинаково обновление памяти в конечном итоге », что делает ваше оборудование для вас (например, протоколы когерентности кэша). Существует только одна память, поэтому основной проблемой является кэширование, которое является частной задачей оборудования.

  2. Этот вопрос кажется неясным. Что имеет значение, так это пара приобретения-выпуска, образованная загрузкой и хранением a_flag, которая является точкой синхронизации и вызывает эффекты thread0 а также thread1 появляться в определенном порядке (т. е. все в thread0 перед магазином случается, перед тем все после цикла в thread1).

  3. Да, иначе у вас не будет точки синхронизации.

  4. Вам не нужны никакие «команды» в C ++. C ++ даже не осознает тот факт, что он работает на каком-либо конкретном типе процессора. Вы могли бы, вероятно, запустить C ++ программу на кубике Рубика с достаточным воображением. C ++ компилятор выбирает необходимые инструкции для реализации поведения синхронизации, описанного моделью памяти C ++, и на x86, которое включает в себя выдачу префиксов блокировки команд и заборов памяти, а также не слишком переупорядочивает инструкции. Поскольку x86 имеет строго упорядоченную модель памяти, приведенный выше код должен производить минимальный дополнительный код по сравнению с наивным, некорректным без атомарного кода.

  5. Имея ваш thread2 в коде делает всю программу неопределенным поведением.


Просто для забавы и чтобы показать, что разработка того, что происходит для вас, может быть поучительным, я скомпилировал код в трех вариантах. (Я добавил glbbal int x И в thread1 я добавил x = data.y;).

Приобретать / Release: (ваш код)

thread0:
mov DWORD PTR data, 1
mov DWORD PTR data+4, 100
mov DWORD PTR data+8, 0
mov DWORD PTR data+12, OFFSET FLAT:.LC0
mov BYTE PTR a_flag, 1
ret

thread1:
.L14:
movzx   eax, BYTE PTR a_flag
test    al, al
je  .L14
mov eax, DWORD PTR data+4
mov DWORD PTR x, eax
ret

Последовательно последовательно: (уберите явный порядок)

thread0:
mov eax, 1
mov DWORD PTR data, 1
mov DWORD PTR data+4, 100
mov DWORD PTR data+8, 0
mov DWORD PTR data+12, OFFSET FLAT:.LC0
xchg    al, BYTE PTR a_flag
ret

thread1:
.L14:
movzx   eax, BYTE PTR a_flag
test    al, al
je  .L14
mov eax, DWORD PTR data+4
mov DWORD PTR x, eax
ret

«Наивные»: (просто используя bool)

thread0:
mov DWORD PTR data, 1
mov DWORD PTR data+4, 100
mov DWORD PTR data+8, 0
mov DWORD PTR data+12, OFFSET FLAT:.LC0
mov BYTE PTR a_flag, 1
ret

thread1:
cmp BYTE PTR a_flag, 0
jne .L3
.L4:
jmp .L4
.L3:
mov eax, DWORD PTR data+4
mov DWORD PTR x, eax
ret

Как видите, нет большой разницы. «Неправильная» версия на самом деле выглядит в основном правильно, за исключением отсутствия загрузки (она использует cmp с операндом памяти). Последовательная последовательная версия скрывает свою дороговизну в xcgh инструкция, которая имеет неявный префикс блокировки и не требует каких-либо явных заборов.

2

Другие решения

Других решений пока нет …