Потоково-локальная синхронизация получения / выпуска
В целом, синхронизация загрузки-накопления / освобождения-хранения является одной из наиболее распространенных форм синхронизации на основе упорядочения памяти в модели памяти C++11. Это в основном, как мьютекс обеспечивает упорядочение памяти. "Критическая секция" между загрузкой-получением и освобождением из хранилища всегда синхронизируется между различными потоками наблюдателей в том смысле, что все потоки-наблюдатели будут согласовывать то, что происходит после получения и до выпуска.
Обычно это достигается с помощью инструкции чтения-изменения-записи, такой как сравнение-обмен, вместе с барьером захвата при входе в критическую секцию, и другой инструкции чтения-изменения-записи с барьером освобождения при выходе из критической секции.
Но есть некоторые ситуации, когда у вас может быть похожий критический раздел[1] между сборкой загрузки и выпуском релиза, за исключением того, что только один поток фактически изменяет переменную синхронизации. Другие потоки могут читать переменную синхронизации, но только один поток фактически изменяет ее. В этом случае при входе в критическую секцию вам не нужна инструкция чтения-изменения-записи. Вам просто нужно простое хранилище, поскольку вы не участвуете в гонках с другими потоками, которые пытаются изменить флаг синхронизации. (Это может показаться странным, но учтите, что многие шаблоны отсрочки восстановления памяти без блокировки, такие как пользовательское пространство RCU или восстановление эпохи, используют локальные переменные синхронизации потоков, которые записываются только одним потоком, но читаются многими потоками, поэтому это не слишком странная ситуация.)
Итак, при входе в критический раздел вы можете просто сделать что-то вроде:
sync_var.store(true, ...);
.... critical section ....
sync_var.store(false, std::memory_order_release);
Нет гонки, потому что, опять же, нет необходимости для чтения-изменения-записи, когда только один поток должен установить / сбросить переменную критического раздела. Другие потоки могут просто прочитать переменную критического раздела с помощью загрузки.
Проблема в том, что, когда вы входите в критическую секцию, вам нужна операция захвата или забор. Но вам не нужно делать нагрузку, только магазин. Так что же является хорошим способом произвести заказ на приобретение, когда вам действительно нужен МАГАЗИН? Я вижу только два реальных варианта, которые попадают в модель памяти C++. Или:
- Используйте
exchange
вместо магазина, так что вы можете сделатьsync_var.exchange(true, std::memory_order_acquire)
, Недостатком является то, что обмен - это более тяжелая операция чтения-изменения-записи, когда все, что вам действительно нужно, - это простое хранилище. Вставьте "фиктивную" загрузку-сборку, например:
(Аннулируются)sync_var.load(станд::memory_order_acquire); sync_var.store(true, std::memory_order_relaxed);
"Макетная" нагрузка кажется лучше. Предположительно, компилятор не может оптимизировать неиспользуемую нагрузку, потому что это атомарная инструкция, побочным эффектом которой является создание отношения "синхронизирует с" с операцией освобождения на sync_var
, Но это также кажется очень глупым, и намерение неясно без комментариев, объясняющих, что происходит.
Итак, каков наилучший способ создания семантики получения, когда все, что нам нужно сделать, это простой магазин?
[1] Я свободно использую термин "критическая секция". Я не обязательно имею в виду раздел, который всегда доступен через взаимное исключение. Скорее, я имею в виду любой раздел, где упорядочение памяти синхронизируется с помощью семантики получения-выпуска. Это может относиться к мьютексу или просто означать что-то вроде RCU, где к критическому разделу могут одновременно обращаться несколько читателей.
1 ответ
Недостаток вашей логики в том, что атомарный RMW не требуется, поскольку данные в критической секции изменяются одним потоком, в то время как все остальные потоки имеют доступ только для чтения.
Это неправда; все еще должен быть четко определенный порядок между чтением и письмом. Вы не хотите, чтобы данные были изменены, пока другой поток все еще читает их. Следовательно, каждый поток должен информировать другие потоки о завершении доступа к данным.
При использовании только атомарного хранилища для входа в критическую секцию невозможно установить связь "синхронизирует с". Синхронизация получения / освобождения основана на зависимости времени выполнения, когда получатель знает, что синхронизация завершена только после наблюдения определенного значения, возвращаемого атомной нагрузкой. Это никогда не может быть достигнуто одним атомарным хранилищем, так как один модифицирующий поток может изменить атомарную переменную sync_var
в любое время и как таковой он не может знать, читает ли еще другой поток данные.
Вариант с "пустышкой" load/acquire
также недействителен, потому что он не может сообщить другим потокам, что он хочет монопольного доступа. Вы пытаетесь решить эту проблему с помощью одного (расслабленного) хранилища, но загрузка и хранилище являются отдельными операциями, которые могут быть прерваны другими потоками (то есть несколько потоков, одновременно обращающихся к критической области).
Атомарный RMW должен использоваться каждым потоком для загрузки определенного значения и в то же время обновлять переменную, чтобы информировать все другие потоки, к которым он теперь имеет эксклюзивный доступ (независимо от того, предназначен ли он для чтения или записи).
void lock()
{
while (sync_var.exchange(true, std::memory_order_acquire));
}
void unlock()
{
sync_var.store(false, std::memory_order_release);
}
Оптимизация возможна, когда несколько потоков имеют доступ для чтения одновременно (например, std::shared_mutex
).