Если ваши данные вписываются в 64-битное значение, большинство систем могут дешево и с атомарной считывать / записывать данные, поэтому просто используйте std::atomic<my_struct>
.
Для smalli sh и / или нечасто Записанные данные , есть несколько способов сделать читателей действительно доступными только для чтения для общих данных, без необходимости выполнения каких-либо операций atomi c RMW на общем счетчике или чем-то еще. Это позволяет масштабировать на стороне чтения многие потоки, при этом читатели не конкурируют друг с другом (в отличие от 128-битного атоми c, считываемого на x86 с использованием lock cmpxchg16b
или с использованием RWlock).
В идеале - просто дополнительный уровень косвенного обращения через указатель atomic<T*>
(RCU) или просто дополнительную загрузку + сравнение и ветвление (SeqLock); нет атомов c RMW или барьеры памяти сильнее, чем acq / rel или что-либо еще на стороне чтения.
Это может быть подходящим для данных, которые очень часто читаются многими потоками, например, отметка времени, обновляемая прерыванием таймера но читать повсюду. Или параметр конфигурации, который обычно никогда не изменяется.
Если ваши данные больше и / или изменяются чаще, одна из стратегий, предложенных в других ответах , требует, чтобы читатель все еще брал RWlock на что-то или атомное увеличение счетчик будет более подходящим. Это не будет идеально масштабироваться, потому что каждому читателю все еще нужно получить эксклюзивное право владения строкой общего кэша, содержащей блокировку или счетчик, чтобы он мог ее изменить, но такого понятия, как бесплатный обед, не существует.
RCU
Похоже, вы на полпути к изобретению RCU (Read Copy Update), где вы обновляете указатель на новую версию.
Но помните, что безблокировочный считыватель может Задержка после загрузки указателя, поэтому у вас есть проблема освобождения. Это сложная часть RCU. В ядре это может быть решено с помощью син c точек, где вы знаете, что нет читателей старше некоторого времени t и, следовательно, можете освободить старые версии. Есть несколько реализаций в пользовательском пространстве. https://en.wikipedia.org/wiki/Read-copy-update и https://lwn.net/Articles/262464/.
Для RCU, чем реже происходят изменения, тем большую структуру данных вы можете оправдать копированием. Например, даже дерево среднего размера может быть выполнимо, если оно когда-либо изменяется интерактивно администратором, в то время как читатели работают на десятках ядер, все проверяют что-то параллельно. например, настройки конфигурации ядра - это то, что RCU отлично подходит для Linux.
SeqLock
Если ваши данные малы (например, 64-битная временная метка на 32-битной машине ), еще одним хорошим вариантом является SeqLock. Читатели проверяют счетчик последовательности до / после не-атомного c копирования данных в личный буфер. Если счетчики последовательности совпадают, мы знаем, что разрывов не было. (Авторы взаимно исключают каждого с отдельным мьютексом). Реализация счетчика с 64-битными атомами c с 32-битными атомами / как реализовать блокировку секвлока с использованием библиотеки c ++ 11 atomi c .
Это немного хакерства в C ++ для написания чего-то, что может эффективно компилироваться в неатомную копию c, которая может иметь разрыв, потому что это неизбежно является гонкой данных UB. (Если только вы не используете std::atomic<long>
с mo_relaxed
для каждого блока отдельно, но затем вы отказываете компилятору в использовании movdqu
или чего-то другого для копирования 16 байтов одновременно.)
A SeqLock заставляет читателя копировать всю вещь (или, в идеале, просто загружать ее в регистры) при каждом чтении, поэтому она всегда подходит только для небольшой структуры или 128-битного целого или чего-то еще. Но для менее чем 64 байта данных это может быть довольно хорошо, лучше, чем когда читатели используют lock cmpxchg16b
для 128-битных данных, если у вас много читателей и нечастые записи.
Хотя это не без блокировки: писатель, который спит во время модификации SeqLock, может получить читателей попробую повторить попытку до бесконечности. Для небольшого SeqLock окно маленькое, и, очевидно, вы хотите, чтобы все данные были готовы до того, как вы выполните первое обновление счетчика последовательности, чтобы минимизировать вероятность прерывания прерывания записи в середине обновления.
В лучшем случае, когда есть только один писатель, поэтому он не должен блокироваться; он знает, что больше ничего не изменит счетчик последовательности.