Является ли std::atomic потокобезопасным для чтения?

Моя цель - только один поток может обрабатывать данные одновременно. Я использовал 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 одновременно, и оба запускают код обработки.

Но я проверял несколько раз, такого случая нет, все просто показывают одно сообщение

обработка ...

Я не нашел ни одного документа об этом, как вы об этом думаете?

Извлечение или установка значения атомарного значения является атомарным и не может быть вытеснено другими потоками. Но как только значение извлекается (или записывается), использование значения не является атомарным. Возьмем, к примеру, if (isRunning) из вашего кода: выборка логического значения является атомарной и потокобезопасной, но использование значения в сравнении может быть вытеснено. Это означает, что один поток может получить значение, другой поток может установить его в значение true, а первый поток использует старое значение false.

Some programmer dude 14.06.2023 07:52

Я думаю, вы ищете compare_exchange

Nathan Pierson 14.06.2023 07:53
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
2
99
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Опубликованный код является потокобезопасным в том смысле, что он не будет вызывать неопределенное поведение, но не потокобезопасным в том смысле, что он не даст вам поведение, которое вы пытаетесь гарантировать.

В частности, в этом коде есть состояние гонки:

if (isRunning)
{
    return;
}
// XXX
isRunning = true;

Представьте себе сценарий, в котором поток A выполняется и доходит до строки с показанным выше комментарием XXX... затем планировщик ОС вытесняет поток A и позволяет потоку B выполняться, а поток B устанавливает isRunning в true и начинает выполнять свои обязанности. обычная обработка. Затем поток A возвращается к ЦП, устанавливает isRunning в true (снова) и также начинает выполнять свою обработку.

В этот момент у вас есть поток A и поток B, обрабатывающие данные одновременно, чего вы пытались избежать.

Вывод таков: для этой цели мьютекс — правильный инструмент для работы; атомарная переменная имеет свое применение, но она не позволит вам (эффективно) заблокировать выполнение одного потока, пока другой поток не завершит выполнение критической секции, и это поведение, которое вы хотите в этой программе.

@wohlstad: я не уверен, что это гонка данных, потому что тип данных атомарный. Атомарное чтение, за которым следует атомарная запись, не является гонкой данных в контексте стандарта ISO. Программа вполне может вести себя неправильно, но это ничем не отличается от банковского приложения, которое снимает 20 долларов с вашего счета и вносит 50 долларов на мой :-) Это логическая ошибка программы, а не нарушение языковых ограничений.

paxdiablo 14.06.2023 09:39

@paxdiablo Я исправляюсь. Упоминание «состояния гонки» автоматически привело меня к ассоциации с UB. Удалил мой комментарий.

wohlstad 14.06.2023 09:42

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% все время, что было бы нехорошо. .

Jeremy Friesner 14.06.2023 08:08

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

Daniel Langr 14.06.2023 08:35

Атомики безопасны для операций, над которыми они имеют атомарность (по определению). Так. например, вам гарантировано, что ничто не изменит atomic, пока вы его читаете.

Однако вашему коду требуется, чтобы вся последовательность чтения/проверки/записи была атомарной, что в данном случае не гарантируется.

Вам нужно либо использовать что-то, что может сделать всю последовательность атомарной (например, мьютекс), либо полагаться на что-то вроде атомарного сравнения и замены.

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

Другие вопросы по теме