Могу ли я заменить атоми c на энергозависимые в очереди с одним читателем / одним писателем? - PullRequest
1 голос
/ 19 февраля 2020

Давайте рассмотрим следующую очередь «один читатель / один писатель», реализованную с помощью связанного списка.

struct queue {
  queue() {
    tail = head = &reserved;
    n = 0;
  }
  void push(item *it) {
     tail->next = it;
     tail = it;
     n++;
  }
  item* pop() {
     while (n == used);
     ++used;
     head = head->next;
     return head;
  }

  item reserved;
  item *tail, *head;

  int used = 0;
  std::atomic <int> n;
}

Теперь я обнаружил, что использование volatile int n может ускорить мой писатель, а я не уверен, гарантирует ли это, что head = head->next всегда может прочитать правильное значение.

ОБНОВЛЕНО: Что, если добавить операцию atomi c между tail->next, n++, то есть

void push(item *it) {
   tail->next = it;
   tail = it;
   a.store(0, std::memory_order_release);
   a.load(std::memory_order_acquire);
   n++;
}

в котором a никогда не доступен читателю? Будет ли это гарантировать порядок tail->next = it и head = head->next? (Тем не менее, он работает быстрее, чем при использовании atomi c n)

Ответы [ 2 ]

2 голосов
/ 19 февраля 2020

Ключевое слово volatile в C ++ не является конструкцией, дающей гарантию того, что чтение / запись переменной гарантируется в таком порядке, как это выглядит в коде, в многопоточных средах. Таким образом, в вашем коде с обнаженным счетчиком шаблона atomi c только с ключевым словом volatile увеличение счетчика, наблюдаемое потоком потребителя, не гарантирует, что item::next также был обновлен.

Чтобы достичь максимальной производительности с гарантией, я думаю, что по крайней мере вы должны вставить барьер записи между обновлением head->next и приращением enet в счетчик, например, на n.fetch_add(1, std::memory_order_release), и барьером чтения непосредственно перед извлечением tail->next, как n.load(std::memory_order_acquire). Однако я не знаю подробностей, касающихся CPU-arch c.

0 голосов
/ 19 февраля 2020

Как уже указывалось в нескольких других комментариях, volatile не связано с многопоточностью, поэтому здесь следует использовать , а не . Однако причина, по которой volatile работает лучше, чем у atmoi c, заключается просто в том, что при использовании volatile ++n означает простую загрузку, в c - инструкции по сохранению, а при использовании атома c - более дорогой. lock xadd (при условии, что вы компилируете для x86).

Но так как это только очередь с одним читателем для одного читателя, вам не нужны дорогие операции чтения-изменения-записи:

struct queue {
  queue() {
    tail = head = &reserved;
    n = 0;
  }
  void push(item *it) {
     tail->next = it;
     tail = it;
     auto new_n = n.load(std::memory_order_relaxed) + 1;
     n.store(new_n, std::memory_order_release);
  }
  item* pop() {
     while (n.load(std::memory_order_acquire) == used);
     ++used;
     head = head->next;
     return head;
  }

  item reserved;
  item *tail, *head;

  int used = 0;
  std::atomic <int> n;
}

Это должно работать так же хорошо, как и изменчивая версия. Если загрузка получения в pop «видит» значение, записанное релизом хранилища в push, эти две операции синхронизируются друг с другом, устанавливая тем самым требуемое отношение «происходит до».

...