Атомарная и безблокировочная запись данных / векторов / массивов произвольного размера

#c #multithreading #atomic

#c #многопоточность #атомарная #seqlock

Вопрос:

Я пытаюсь реализовать следующую функциональность:

  • Атомарная и безблокировочная запись или чтение-изменение-запись типа данных произвольного размера (в моем случае обычно вектор float / int с числом элементов до 6).
  • Атомарное чтение из вышеуказанного типа данных, которое не блокирует поток записи. Операция чтения может быть заблокирована операцией записи.

Пример использования: я пытаюсь написать программное обеспечение для станка с ЧПУ. Шаговые импульсы для двигателей генерируются программным обеспечением в потоке реального времени. Этот поток реального времени постоянно обновляет переменную, которая содержит текущее положение оси. Несколько других потоков, не относящихся к реальному времени, могут считывать эту переменную, например, для отображения текущей позиции.

Вопрос 1: Существует ли стандартное / принятое решение или шаблон для такого рода проблем?

Мне пришла в голову следующая идея: используйте an std::atomic<uint64_t> для защиты данных и отслеживания погоды, которую поток в данный момент записывает (путем проверки последнего бита) или записал с момента начала чтения (путем увеличения значения при записи).

 template <class DATA, class FN>
void read_modify_write(DATAamp; data, std::atomic<uint64_t>amp; protector, FN fn)
{
  auto old_protector_value = protector.load();
  do
  {
    // wait until no other thread is writing
    while(old_protector_value % 2 != 0)
      old_protector_value = protector.load();

    // try to acquire write privileges
  } while(!protector.compare_exchange_weak(old_protector_value, old_protector_value   1));

  // write data
  data = fn(data);

  // unlock
  protector = old_protector_value   2;
};

template <class DATA>
DATA read(const DATAamp; data, std::atomic<uint64_t>amp; protector)
{
  while(true)
  {
    uint64_t old_protector_value = protector.load();

    // wait until no thread is writing
    while(old_protector_value % 2 != 0)
      old_protector_value = protector.load();

    // read data
    auto ret = data;

    // check if data has changed in the meantime
    if(old_protector_value == protector)
      return ret;
  }
}
  

Вопрос 2: Является ли приведенный выше код потокобезопасным и удовлетворяет вышеуказанным требованиям?

Вопрос 3: Можно ли его улучшить?

(Единственная теоретическая проблема, которую я смог найти, заключается в том, что счетчик обтекается, т. Е. За 1 операцию чтения выполняется ровно 2 ^ 63 операции записи. Я бы счел эту слабость приемлемой, если нет лучших решений.)

Спасибо

Комментарии:

1. Я думаю, что если вы пытаетесь реализовать атомарное чтение / запись для типа данных произвольного размера, то в конечном итоге вы будете использовать блокировки, неявно или явно. Атомарный и безблокировочный доступ являются особенностью аппаратного обеспечения.

2. @curiousguy Конечно. Он просто не может быть одновременно атомарным и безблокировочным.

3. @curiousguy Вам понадобится мьютекс или какое-либо другое устройство синхронизации. Возможность атомарного изменения фрагмента памяти без использования дополнительной явной синхронизации — это функция, предоставляемая аппаратным обеспечением. x86, например, разрешает собственный синхронный доступ (через префикс БЛОКИРОВКИ) только для размеров данных, обычно обрабатываемых инструкциями, например, 1, 2, 4, 8 байт, хотя на аппаратномуровень реализации Я думаю, что он синхронизируется на 64-байтовых страницах. Я почти уверен, что в аппаратном обеспечении нет, скажем, 256-байтовой возможности синхронизации.

4. @curiousguy Как и в ответе mpoeter, код OP по сути реализует программную блокировку. Следовательно, не безблокировочная, какой бы атомной она ни была. Кроме того, возможно, стоит упомянуть, что безблокировочная запись относится только к программным блокировкам, поскольку аппаратное обеспечение само может иметь какие-то блокировки более низкого уровня.

5. @Anonymous1847 Ой, я думаю, что здесь была проблема с английским языком! Я прочитал «Атомарный и безблокировочный доступ» как «Атомарный доступ, а также безблокировочный доступ, (…)», поэтому фраза будет применяться к обоим доступам. Но это означало «доступы, которые являются КАК атомарными, так и безблокировочными»!!! Мой плохой!

Ответ №1:

Строго говоря, ваш код не свободен от блокировок, потому что вы эффективно используете LSB protector для реализации блокировки.

Ваше решение очень похоже на блокировку последовательности. Однако фактическая операция чтения auto ret = data; , строго говоря, является гонкой данных. Честно говоря, просто невозможно написать полностью совместимый со стандартом seqlock на C 17, для этого нам нужно дождаться C 20.

Можно расширить seqlock, чтобы сделать операции чтения без блокировки за счет более высокого использования памяти. Идея состоит в том, чтобы иметь несколько экземпляров данных (назовем их слотами), и операция записи всегда выполняет циклическую запись в следующий слот. Это позволяет операции чтения считывать данные из последнего полностью записанного слота. Дмитрий Вьюков описал свой подход в улучшенном безблокировочном SeqLock. Вы можете взглянуть на мою реализацию seqlock, которая является частью моей библиотеки xenium. Он также дополнительно допускает операции чтения без блокировки с настраиваемым количеством слотов (хотя он немного отличается от Vyukov тем, как читатель находит последний полностью записанный слот).

Комментарии:

1. Большое вам спасибо. Я не знал о «блокировках последовательности», это была именно та информация, которую мне не хватало.

2. К сожалению, это немного сложно сделать правильно. Если вы хотите самостоятельно реализовать блокировку, я рекомендую взглянуть на эту презентацию Ханса-Дж. Boehm: Могут ли Seqlocks уживаться с моделями памяти языка программирования ? . К сожалению, я смог найти только ссылку на слайды, ссылка на сопроводительный технический отчет на странице HP мертва.