Можно ли читать и проверять общую память без мьютексов?

в Linux я использую shmget и shmat чтобы настроить сегмент общей памяти, в который будет записываться один процесс, а один или несколько процессов будут считываться. Данные, которые совместно используются, имеют размер несколько мегабайт и при обновлении полностью переписываются; они никогда не обновляются частично.

у меня есть сегмент общей памяти, изложенный следующим образом:

    -------------------------
    | t0 | actual data | t1 |
    -------------------------

где t0 и t1 копии время, когда писатель начал свое обновление (с достаточной точностью, чтобы последующие обновления гарантированно имели разное время). Писатель сначала пишет t1, затем копирует данные, затем записывает в t0. Читатель, с другой стороны, читает t0, затем данные, затем t1. Если читатель получает то же значение для t0 и t1 затем он считает данные согласованными и действительными, если нет, он пытается снова.

это процедура гарантирует, что если читатель считает данные действительными, то это действительно так?

мне нужно беспокоиться о выполнении вне заказа (OOE)? Если да, то будет ли читатель использовать memcpy чтобы получить весь сегмент общей памяти преодолеть проблемы OOE на стороне читателя? (Это предполагает, что memcpy выполняет его копирование линейно и по возрастанию через адресное пространство. Действительно ли это предположение?)

2 ответов


Джо Даффи дает точно такой же алгоритм и называет его: "масштабируемая схема чтения / записи с оптимистичной повторной попыткой".

это работает.
Вам нужно два поля порядкового номера.

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

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

какие инструкции необходимы для достижения этого, зависит от архитектуры. Е. Г. на х86/х64, из-за относительно надежные гарантии в этом конкретном случае не нужны никакие барьеры машины специфические на всех*.

* по-прежнему необходимо убедиться, что компилятор / JIT не возится с нагрузками и магазины, например, с помощью volatile (Это имеет другое значение в Java и C#, чем в ISO C/C++. Составители, однако, может различаться. Е. Г. с помощью VC++ 2005 или выше с летучими было бы безопасно делать выше. Вижу "Microsoft Specific" раздел. Это можно сделать и с другими компиляторами на x86 / x64. Выделенный код сборки должен быть проверен, и необходимо убедиться, что доступ к t0 и Т1 не устранены или переехали вокруг компилятора.)

в качестве примечания, если вам когда-нибудь понадобится MFENCE, lock or [TopOfStack],0 может быть лучшим вариантом, в зависимости от ваших потребностей.


современном железе вообще ничего, но последовательно согласуется. Таким образом, это не гарантирует работу как таковую, если вы не выполняете барьеры памяти в соответствующих местах. Барьеры необходимы, поскольку архитектура реализует более слабую модель согласованности общей памяти, чем последовательная согласованность. Это как таковое не имеет ничего общего с конвейеризацией или OoO, но позволяет нескольким процессорам эффективно получать доступ к системе памяти параллельно. См., например,согласованность общей памяти модели: учебник. На однопроцессорных, вам не нужны барьеры, потому что весь код выполняется последовательно на одном процессоре.

кроме того, нет необходимости иметь два поля времени, количество последовательностей, вероятно, лучший выбор, поскольку нет необходимости беспокоиться о том, что два обновления настолько близки, что они получают одинаковую метку времени, и обновление счетчика намного быстрее, чем получение текущего времени. Кроме того, нет никаких шансов, что часы движутся назад во времени, что может произойти например, когда ntpd настраивается для дрейфа часов. Хотя эту последнюю проблему можно преодолеть в Linux с помощью clock_gettime (CLOCK_MONOTONIC, ...). Другим преимуществом использования счетчиков последовательности вместо временных меток является то, что вам нужен только один счетчик последовательности. Записывающее устройство увеличивает счетчик как до записи данных, так и после записи. Затем считыватель считывает порядковый номер, проверяет, что он четный, и если да, то считывает данные, и, наконец, снова считывает порядковый номер и сравнивает его с первый порядковый номер. Если порядковый номер нечетный, это означает, что запись выполняется, и нет необходимости читать данные.

ядро Linux использует примитив блокировки под названием seqlocks что сделать что-то вроде выше. Если вы не боитесь "загрязнения GPL", вы можете google для реализации; как таковой это тривиально, но трюк в том, чтобы получить барьеры правильно.