Моя цель - только один поток может обрабатывать данные одновременно. Я использовал std::mutex, std::unique_lock, try_lock работает очень хорошо.
Но кто-то возразил, почему бы просто не использовать атомарную переменную? Что-то вроде этого,
#include <atomic>
#include <thread>
std::atomic_bool isRunning = false;
void process() {
if (isRunning)
{
return;
}
isRunning = true;
cout << "processing ..." << endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
cout << "finished" << endl;
isRunning = false;
}
int main(int argc, wchar_t* argv[])
{
std::thread th1(process);
std::thread th2(process);
std::thread th3(process);
std::thread th4(process);
std::thread th5(process);
th1.join();
th2.join();
th3.join();
th4.join();
th5.join();
}
По моему опыту, atomic безопасен для записи, но не безопасен для чтения. Например, if (isRunning), может быть, 2 потока получают одно и то же значение false одновременно, и оба запускают код обработки.
Но я проверял несколько раз, такого случая нет, все просто показывают одно сообщение
обработка ...
Я не нашел ни одного документа об этом, как вы об этом думаете?
Я думаю, вы ищете compare_exchange





Опубликованный код является потокобезопасным в том смысле, что он не будет вызывать неопределенное поведение, но не потокобезопасным в том смысле, что он не даст вам поведение, которое вы пытаетесь гарантировать.
В частности, в этом коде есть состояние гонки:
if (isRunning)
{
return;
}
// XXX
isRunning = true;
Представьте себе сценарий, в котором поток A выполняется и доходит до строки с показанным выше комментарием XXX... затем планировщик ОС вытесняет поток A и позволяет потоку B выполняться, а поток B устанавливает isRunning в true и начинает выполнять свои обязанности. обычная обработка. Затем поток A возвращается к ЦП, устанавливает isRunning в true (снова) и также начинает выполнять свою обработку.
В этот момент у вас есть поток A и поток B, обрабатывающие данные одновременно, чего вы пытались избежать.
Вывод таков: для этой цели мьютекс — правильный инструмент для работы; атомарная переменная имеет свое применение, но она не позволит вам (эффективно) заблокировать выполнение одного потока, пока другой поток не завершит выполнение критической секции, и это поведение, которое вы хотите в этой программе.
@wohlstad: я не уверен, что это гонка данных, потому что тип данных атомарный. Атомарное чтение, за которым следует атомарная запись, не является гонкой данных в контексте стандарта ISO. Программа вполне может вести себя неправильно, но это ничем не отличается от банковского приложения, которое снимает 20 долларов с вашего счета и вносит 50 долларов на мой :-) Это логическая ошибка программы, а не нарушение языковых ограничений.
@paxdiablo Я исправляюсь. Упоминание «состояния гонки» автоматически привело меня к ассоциации с UB. Удалил мой комментарий.
mutex — это инструмент, разработанный для этой работы, поэтому, если вы уже используете его, нет причин использовать вместо него atomic. В зависимости от реализации mutex может быть более эффективным или просыпаться быстрее.
При этом вы можете использовать atomic в качестве блокировки, но недостаточно использовать isRunning = true, потому что несколько потоков могут обойти if (isRunning) до того, как вы установите значение. Вместо этого вы должны использовать compare_exchange_weak для сравнения и установки атомарным образом:
// Before the critical section
bool expected;
do {
expected = false;
} while (!isRunning.compare_exchange_weak(expected, true));
// Critical section here
// Unlock at the end
isRunning = false;
Это потребует много процессорного времени, потому что он постоянно проверяет значение. Вы можете добавить счетчик, чтобы дать время другим потокам на случай, если ожидание займет много времени:
bool expected;
size_t counter = 0;
do {
expected = false;
if (counter > 100) {
Sleep(10);
}
else if (counter > 20) {
Sleep(5);
}
else if (counter > 3) {
Sleep(1);
}
counter++;
} while (!isRunning.compare_exchange_weak(expected, true));
Возможно, вы захотите подчеркнуть, что реализация, показанная в этом ответе, представляет собой спин-блокировку и поэтому будет эффективной только в том случае, если ни один поток никогда не остается в критической секции в течение значительного периода времени. В программах, где критическая секция занимает какое-то значительное количество времени (например, выполнение ввода-вывода или расширенные вычисления), вы в конечном итоге получите, что все остальные потоки закрепляют каждое ядро на 100% все время, что было бы нехорошо. .
Более того, эффективные спин-блокировки не вращаются и не обмениваются (сравниваются), поскольку это связано с конкуренцией за кэш. Вместо этого они вращаются при чтении. (Плюс также позаботьтесь о порядке памяти.)
Атомики безопасны для операций, над которыми они имеют атомарность (по определению). Так. например, вам гарантировано, что ничто не изменит atomic, пока вы его читаете.
Однако вашему коду требуется, чтобы вся последовательность чтения/проверки/записи была атомарной, что в данном случае не гарантируется.
Вам нужно либо использовать что-то, что может сделать всю последовательность атомарной (например, мьютекс), либо полагаться на что-то вроде атомарного сравнения и замены.
Просто имейте в виду, что если ваш код будет зацикливаться до тех пор, пока не сработает сравнение и замена, на самом деле нет большой разницы между этим и мьютексом. На самом деле мьютекс может быть даже предпочтительнее, поскольку он может переводить потоки в спящий режим до тех пор, пока мьютекс не будет освобожден, а не запускать их все, ожидая освобождения ресурса.
Извлечение или установка значения атомарного значения является атомарным и не может быть вытеснено другими потоками. Но как только значение извлекается (или записывается), использование значения не является атомарным. Возьмем, к примеру,
if (isRunning)из вашего кода: выборка логического значения является атомарной и потокобезопасной, но использование значения в сравнении может быть вытеснено. Это означает, что один поток может получить значение, другой поток может установить его в значение true, а первый поток использует старое значение false.