Атомарные операции и модель памяти
Как разделять данные между потоками без блокировок и делать это правильно
std::atomic<T> даёт операции, которые неделимы (ни один поток не увидит наполовину записанное значение) и, что не менее важно, позволяет управлять порядком, в котором записи одного потока становятся видимыми другому. Тема о обеих половинах: об атомарности и о модели памяти, которая определяет слово "видимо".
- точно сформулировать, что такое гонка данных и почему это неопределённое поведение;
- использовать
std::atomic<T>-load,store,exchange,fetch_add,compare_exchange; - объяснить отношение happens-before и то, как оно делает запись видимой;
- на уровне интуиции рассуждать о моделях порядка
seq_cst,acquire/releaseиrelaxed.
1. Гонка данных - это неопределённое поведение
Гонка данных возникает, когда два потока обращаются к одной ячейке памяти одновременно, хотя бы одно обращение является записью, и эти обращения не упорядочены никакой синхронизацией. Стандарт C++ говорит, что программа с гонкой данных имеет неопределённое поведение (UB). Это сильнее, чем "можно прочитать устаревшее значение": компилятору и процессору разрешено считать, что гонок не бывает, поэтому гонка может дать "рваное" значение, потерянное обновление или оптимизацию кода способом, лишённым последовательного смысла.
int counter = 0; // обычный int, общий
void worker() {
for (int i = 0; i < 100000; ++i)
++counter; // чтение-изменение-запись, НЕ атомарно -> ГОНКА
}
// Два потока, выполняющих worker(), гоняются за counter.
// Итоговое значение непредсказуемо, а программа - UB.
++counter выглядит как один шаг, но на деле это три: загрузка, прибавление единицы, запись. Два потока могут оба загрузить одно значение, оба прибавить единицу и оба записать - так два инкремента превращаются в один. Оставить counter обычным int без защиты - это не просто риск неверной суммы; это гонка, а гонки - это UB.
std::mutex тоже не гоняются - блокировка задаёт порядок.