спокойный порядок и видимость между нитями - PullRequest
0 голосов
/ 06 июля 2019

Я узнал от расслабленного упорядочивания в качестве сигнала , что хранилище атомарной переменной должно быть видно для другого потока в «разумном количестве времени».

То естьЯ уверен, что это должно произойти в очень короткое время (нано секунда?).Однако я не хочу полагаться на «в разумные сроки».

Итак, вот некоторый код:

std::atomic_bool canBegin{false};
void functionThatWillBeLaunchedInThreadA() {
    if(canBegin.load(std::memory_order_relaxed))
        produceData();
}

void functionThatWillBeLaunchedInThreadB() {
    canBegin.store(true, std::memory_order_relaxed);
}

Потоки A и B находятся в некотором родеThreadPool, поэтому в этой задаче нет создания потока или чего-либо еще.Мне не нужно защищать какие-либо данные, поэтому порядок получения / потребления / выпуска для атомарного хранилища / загрузки здесь не нужен (я думаю?).

Мы точно знаем, что функция functionThatWillBeLaunchedInThreadA будетзапускается ПОСЛЕ конца functionThatWillBeLaunchedInThreadB.

Однако в таком коде у нас нет никакой гарантии, что хранилище будет видно в потоке A, поэтому поток A может прочитать устаревшее значение(false).

Вот некоторые решения, о которых я думаю.

Решение 1: Используйте волатильность

Просто объявите volatile std::atomic_bool canBegin{false}; Здесь волатильность гарантирует нам, что мы будемне видят устаревшего значения.

Решение 2: Используйте мьютекс или спин-блокировку

Здесь идея заключается в том, чтобы защитить доступ canBegin через мьютекс / спин-блокировку, которые гарантируют через порядок отпускания / получения, который мы не будемувидеть устаревшее значение.Мне не нужно, чтобы canGo был атомарным.

Решение 3: совсем не уверен, но забор памяти?

Может быть, этот код не будет работать, поэтому, скажите мне:).

bool canGo{false}; // not an atomic value now
// in thread A
std::atomic_thread_fence(std::memory_order_acquire);
if(canGo) produceData();

// in thread B
canGo = true;
std::atomic_thread_fence(std::memory_order_release);

Для ссылки на cpp для этого случая записывается следующее:

все неатомарные и релаксированные атомарные хранилища, которые упорядочены до FB в потоке Bпроизойдет - до того, как все неатомарные и расслабленные атомные нагрузки из тех же мест, что и в потоке A, после FA

Какое решение вы бы использовали и почему?

Ответы [ 2 ]

0 голосов
/ 09 июля 2019

Мы точно знаем, что functionThatWillBeLaunchedInThreadAfunction будет запущен ПОСЛЕ окончания functionThatWillBeLaunchedInThreadB.

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

В ответ на ответ ...

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

std::atomic_bool canBegin{false};

void functionThatWillBeLaunchedInThreadA() {
    if(canBegin.load(std::memory_order_acquire))
        produceData();
}

void functionThatWillBeLaunchedInThreadB() {
    canBegin.store(true, std::memory_order_release);
}

Кстати, разве это не должно быть циклом while?

void functionThatWillBeLaunchedInThreadA() {
    while (!canBegin.load(std::memory_order_acquire))
    { }
    produceData();
}

Мне не нужно защищатьданные, поэтому порядок получения / потребления / выпуска для атомарного хранилища / загрузки здесь не нужен (я думаю?)

В этом случае порядок необходим для того, чтобы подсистема компилятора / ЦП / памяти незаказ магазина canBegin true до завершения предыдущих операций чтения / записи.И он должен фактически останавливать ЦП, пока не будет гарантировано, что каждая запись, предшествующая порядку программы, будет распространяться до сохранения до canBegin.На стороне загрузки это предотвращает чтение / запись памяти до того, как canBegin будет прочитано как true.Если я правильно помню, это переупорядочение на самом деле не произойдет на любом современном оборудовании в этом конкретном случае из-за зависимости данных.(Я не уверен, как это играет с умозрительным исполнением).Расслабленный порядок в памяти не дает ни одной из этих гарантий упорядочения.

Однако в таком коде у нас нет никакой гарантии, что хранилище будет видно в потоке A, поэтому поток A может прочитатьустаревшее значение (false).

Вы сами сказали:

хранилище атомарной переменной должно быть видно для другого потока в "в течение разумного промежутка времени".

Даже при неупорядоченном порядке памяти запись гарантированно в конечном итоге достигнет других ядер, и все ядра в конечном итоге согласятся с историей хранения любой заданной переменной, поэтому устаревшие значения отсутствуют.Есть только значения, которые еще не распространились.Что «расслаблено» в этом, так это порядок хранения по отношению к другим переменным.Таким образом, memory_order_relaxed решает проблему устаревшего чтения (но не учитывает порядок, требуемый, как описано выше).

Не используйте volatile.Он не обеспечивает всех гарантий, требуемых для атомарности в модели памяти C ++, поэтому его использование будет неопределенным поведением.См. https://en.cppreference.com/w/cpp/atomic/memory_order#Relaxed_ordering внизу, чтобы прочитать об этом.

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

Ограничение памяти также будет работать, но ваши ограждения находятся в неправильном месте (это противоречит интуиции), и переменная связи между потоками должна быть std::atomic.(Осторожно, играя в эти игры ...! Поведение с неопределенным поведением легко определить) Благодаря оградам можно спокойно упорядочивать.

std::atomic<bool> canGo{false}; // MUST be atomic

// in thread A
if(canGo.load(std::memory_order_relaxed))
{
    std::atomic_thread_fence(std::memory_order_acquire);
    produceData();
}

// in thread B
std::atomic_thread_fence(std::memory_order_release);
canGo.store(true, memory_order_relaxed);

Заборы памяти на самом деле более строгие, чем порядок получения / выпуска на std::atomic load / store, так что это ничего не дает и может стоить дороже.

Похоже, вы действительно хотите избежать накладных расходов с помощью вашего механизма сигнализации.Это как раз то, для чего была изобретена семантика получения / выпуска std::atomic!Вы слишком беспокоитесь о устаревших ценностях.Да, атомарный RMW даст вам «последнюю» ценность, но они также являются очень дорогими операциями.Я хочу дать вам представление о том, как быстро происходит приобретение / выпуск.Скорее всего, вы ориентируетесь на x86.В x86 общий порядок хранения, а загрузки / хранилища размером в слово являются атомарными, поэтому сборка загрузки компилируется только в обычную загрузку, а хранилище релизов компилируется в обычное хранилище.Так что получается, что почти все в этом длинном посте, вероятно, все равно будет компилироваться с точно таким же кодом.

0 голосов
/ 06 июля 2019

Благодаря комментарию @CuriouslyRecurringThoughts и ответу Энтони Уильямса, я думаю, что единственный способ справиться с такой вещью - это использовать операцию CAS (которая является операцией чтения-изменения-записи)

Итак, мыв конечном итоге с этим

std::atomic_bool canBegin{false};
void functionThatWillBeLaunchedInThreadA() {
    bool expected = true;
    canBegin.compare_exchange_strong(expected, true, std::memory_order_relaxed);
    if(expected)
        produceData();
}

void functionThatWillBeLaunchedInThreadB() {
    canBegin.store(true, std::memory_order_relaxed);
}

Это, наконец, довольно сложно ...

...