Стоит ли объявлять size_t как std::atomic, если он используется в двух потоках?

У меня есть переменная size_t, которая обновляется std::thread и читается другим std::thread.

Я знаю, что могу защитить чтение и запись с помощью мьютекса. Но будет ли это то же самое или будет лучше, если я сделаю size_t как std::atomic<size_t>?


person AdeleGoldberg    schedule 18.01.2020    source источник
comment
ИМХО, атомы хороши, но вы должны быть осторожны, используя их правильно. Например. Инкремент/декремент должен быть реализован правильно. Взгляните на это: stackoverflow.com/questions/15056237/   -  person harandk    schedule 18.01.2020
comment
и что: stackoverflow.com/questions/31978324/what-exactly-is -stdatomic   -  person harandk    schedule 18.01.2020


Ответы (1)


Да, оно того стоит. На самом деле обязательно использовать std::atomic или синхронизировать доступ к неатомарному, если несколько потоков используют одну и ту же переменную и хотя бы один из них выполняет запись в переменную. Несоблюдение этого правила является неопределенным поведением гонки данных.

В зависимости от того, как вы используете std::size_t, компилятор может предположить, что неатомарные и иным образом несинхронизированные переменные не будут меняться из других потоков и соответствующим образом оптимизировать код. Это может привести к возникновению Bad Things™.

Мой обычный пример для этого — цикл, в котором используется неатомарное логическое значение:

// make keepRunning an std::atomic<bool> to avoid endless loop
bool keepRunning {true};
unsigned number = 0;

void stop()
{
    keepRunning = false;
}

void loop()
{
    while(keepRunning) {
        number += 1;
    }
}

При компиляции этого кода с включенной оптимизацией GCC и Clang будут проверять keepRunning только один раз, а затем запускать бесконечный цикл. См. https://godbolt.org/z/GYMiLE для сгенерированного ассемблера.

то есть они оптимизируют его в if (keepRunning) infinite_loop;, снимая нагрузку с цикла. Поскольку он не является атомарным, им разрешено предполагать, что никакой другой поток не может его писать. См. Многопоточная программа застряла в оптимизированном режиме, но работает нормально в -O0 для более подробного рассмотрения той же проблемы.

Обратите внимание, что этот пример показывает ошибку только в том случае, если тело цикла достаточно простое. Однако неопределенное поведение все еще присутствует, и его следует избегать, используя std::atomic или синхронизацию.


В этом случае вы можете использовать std::atomic<bool> с std::memory_order_relaxed, потому что вам не нужна синхронизация или упорядочение. другие операции в потоке записи или чтения. Это даст вам атомарность (без разрывов) и предположение, что значение может изменяться асинхронно, не заставляя компилятор использовать какие-либо инструкции барьера asm для создания большего порядка в отношении. другие операции.

Таким образом, можно и безопасно использовать атомарные объекты без какой-либо синхронизации и даже без создания синхронизации между модулем записи и чтения, как это происходит при загрузке и сохранении seq_cst или получении/выпуске. Вы можете использовать эту синхронизацию для безопасного совместного использования неатомарной переменной или массива, например. с atomic<int*> buffer, который читатель читает, когда указатель не равен NULL.

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

person Finn    schedule 18.01.2020
comment
Не обязательно синхронизировать, вам разрешено использовать memory_order_relaxed, чтобы программа чтения просто считывала текущее значение. И это не требует дополнительных ассемблерных инструкций для любой архитектуры; дополнительные инструкции предназначены только для того, чтобы заставить поток ждать до/после чтения, чтобы упорядочить его. другие загрузки или сохранения в том же потоке. Все ISA запускают std::thread только между ядрами, которые согласованы с кешем, поэтому, если вам не нужен какой-либо порядок, просто атомарность, вам просто нужны обычные инструкции загрузки/сохранения (для достаточно маленького выровненного объекта). - person Peter Cordes; 18.01.2020
comment
Что вы подразумеваете под атомарным доступом к памяти? Что такое неатомарный доступ на уровне asm? - person curiousguy; 19.01.2020
comment
@curiousguy: например. у вас может быть 8-байтовая структура, выровненная только по 4 байтам. К нему можно (и часто можно) получить доступ в одной инструкции на x86-64, но если это выходит за границу строки кэша, он не будет атомарным даже на процессорах Intel. А на AMD это гарантированно атомарно только в том случае, если оно не пересекает 8-байтовую границу. - person Peter Cordes; 19.01.2020