std :: atomic <int>memory_order_relaxed VS volatile sig_atomic_t в многопоточной программе - PullRequest
6 голосов
/ 14 июня 2019

Дает ли volatile sig_atomic_t какие-либо гарантии порядка памяти?Например, если мне нужно просто загрузить / сохранить целое число, можно ли его использовать?

Например, здесь:

volatile sig_atomic_t x = 0;
...
void f() {
  std::thread t([&] {x = 1;});
  while(x != 1) {/*waiting...*/}
  //done!
}

это правильный код?Существуют ли условия, при которых он может не работать?

Примечание. Это слишком упрощенный пример, т.е. я не ищу лучшего решения для данного фрагмента кода.Я просто хочу понять, какое поведение я могу ожидать от volatile sig_atomic_t в многопоточной программе в соответствии со стандартом C ++.Или, если это так, поймите, почему поведение не определено.

Я нашел следующее утверждение здесь :

Тип библиотеки sig_atomic_t не делаетобеспечьте синхронизацию между потоками или упорядочение памяти, только атомарность.

И если я сравню это с этим определением здесь :

memory_order_relaxed: Relaxed операция: нет ограничений синхронизации или упорядочения, налагаемых на другие операции чтения или записи, гарантируется только атомарность этой операции

Разве это не то же самое?Что именно здесь означает атомарность ?volatile делает здесь что-нибудь полезное?В чем разница между «не обеспечивает синхронизацию или упорядочение памяти» и «нет ограничений по синхронизации или упорядочению»?

1 Ответ

8 голосов
/ 14 июня 2019

Вы используете объект типа sig_atomic_t, доступ к которому осуществляется двумя потоками (с одним изменением).
Для модели памяти C ++ 11 это неопределенное поведение, и простое решение заключается в использовании std::atomic<T>

std::sig_atomic_t и std::atomic<T> находятся в разных лигах. В коде portable один не может быть заменен другим, и наоборот.

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

sig_atomic_t не имеет свойств между потоками.Фактически, если объект этого типа доступен (изменен) более чем одним потоком (как в вашем примере кода), это технически неопределенное поведение (гонка данных);Поэтому свойства упорядочения памяти между потоками не определены.

для чего используется sig_atomic_t?

Объект этого типа может использоваться в обработчике сигналов, но только если он объявлен volatile.Атомарность и volatile гарантируют 2 вещи:

  • атомарность: обработчик сигнала может асинхронно сохранять значение для объекта, и любой, кто читает одну и ту же переменную (в том же потоке), может наблюдать только доили после значения.
  • volatile: хранилище не может быть «оптимизировано» компилятором и поэтому видимо (в том же потоке) в (или после) точке, где сигнал прервал выполнение.

Например:

volatile sig_atomic_t quit {0};

void sig_handler(int signo)  // called upon arrival of a signal
{
    quit = 1;  // store value
}


void do_work()
{
    while (!quit)  // load value
    {
        ...
    }
}

Хотя этот код является однопоточным, do_work может быть прерван асинхронно с помощью сигнала, который запускает sig_handler и атомно изменяет значение quit,Без volatile компилятор может «поднять» нагрузку из quit из цикла while, делая невозможным для do_work наблюдение изменения quit, вызванного сигналом.

Почему std::atomic<T> нельзя использовать в качестве замены std::sig_atomic_t?

Вообще говоря, шаблон std::atomic<T> - это другой тип, потому что он предназначен для одновременного доступа несколькихпотоков и обеспечивает межпотоковые гарантии заказа.Атомарность не всегда доступна на уровне ЦП (особенно для больших типов T), и поэтому реализация может использовать внутреннюю блокировку для эмуляции атомарного поведения.Использование std::atomic<T> блокировки для определенного типа T доступно через функцию-член is_lock_free() или константу класса is_always_lock_free (C ++ 17).

Проблема с использованием этого типа вОбработчик сигнала заключается в том, что стандарт C ++ не гарантирует, что std::atomic<T> не блокируется для любого типа T.Только std::atomic_flag имеет такую ​​гарантию, но это другой тип.

Представьте приведенный выше код, где флаг quit представляет собой std::atomic<int>, который не блокируется.Есть вероятность, что когда do_work() загружает значение, оно прерывается сигналом после получения блокировки, но перед ее снятием.Сигнал вызывает sig_handler(), который теперь хочет сохранить значение в quit, взяв ту же блокировку, которая уже была получена do_work, упс.Это неопределенное поведение и, возможно, вызывает взаимоблокировку.
std::sig_atomic_t не имеет этой проблемы, потому что он не использует блокировку.Все, что нужно, это тип, который неделим на уровне ЦП и на многих платформах, он может быть простым:

typedef int sig_atomic_t;

Суть в том, чтобы использовать volatile std::sig_atomic_t для обработчиков сигналов в одном потокеи используйте std::atomic<T> как тип без данных в многопоточном окружении.

...