Алгоритм очереди без блокировки, повторное чтение для согласованности - PullRequest
2 голосов
/ 06 октября 2010

Я изучаю алгоритмы без блокировки (en-, de) очереди Майкла и Скотта . Проблема в том, что я не могу объяснить / понять (как и в статье, кроме комментариев в самом коде) пару строк.

Ставить:

  enqueue(Q: pointer to queue_t, value: data type)
   E1:   node = new_node()        // Allocate a new node from the free list
   E2:   node->value = value      // Copy enqueued value into node
   E3:   node->next.ptr = NULL    // Set next pointer of node to NULL
   E4:   loop                     // Keep trying until Enqueue is done
   E5:      tail = Q->Tail        // Read Tail.ptr and Tail.count together
   E6:      next = tail.ptr->next // Read next ptr and count fields together
   E7:      if tail == Q->Tail    // Are tail and next consistent?
               // Was Tail pointing to the last node?
   E8:         if next.ptr == NULL
                  // Try to link node at the end of the linked list
   E9:            if CAS(&tail.ptr->next, next, <node, next.count+1>)
  E10:               break        // Enqueue is done.  Exit loop
  E11:            endif
  E12:         else               // Tail was not pointing to the last node
                  // Try to swing Tail to the next node
  E13:            CAS(&Q->Tail, tail, <next.ptr, tail.count+1>)
  E14:         endif
  E15:      endif
  E16:   endloop
         // Enqueue is done.  Try to swing Tail to the inserted node
  E17:   CAS(&Q->Tail, tail, <node, tail.count+1>)

Зачем нужен E7? От этого зависит правильность? Или это просто оптимизация? Это if может завершиться ошибкой, если другой поток успешно выполнил E17 или D10 ниже (и изменил Q-> Tail), в то время как первый поток выполнил E5, но еще не E7. Но что, если E17 выполняется сразу после того, как первый поток выполняет E7?

edit : Доказывает ли это последнее предложение, что E7 не может быть больше, чем оптимизация? Моя интуиция заключается в том, что это делает , так как я привожу сценарий, в котором «очевидно», оператор if примет неверное решение, хотя алгоритм все равно должен работать правильно. Но тогда мы могли бы заменить условие if случайным условием, не влияя на правильность. Есть ли дыра в этом аргументе?

Dequeue:

dequeue(Q: pointer to queue_t, pvalue: pointer to data type): boolean
   D1:   loop                          // Keep trying until Dequeue is done
   D2:      head = Q->Head             // Read Head
   D3:      tail = Q->Tail             // Read Tail
   D4:      next = head.ptr->next      // Read Head.ptr->next
   D5:      if head == Q->Head         // Are head, tail, and next consistent?
   D6:         if head.ptr == tail.ptr // Is queue empty or Tail falling behind?
   D7:            if next.ptr == NULL  // Is queue empty?
   D8:               return FALSE      // Queue is empty, couldn't dequeue
   D9:            endif
                  // Tail is falling behind.  Try to advance it
  D10:            CAS(&Q->Tail, tail, <next.ptr, tail.count+1>)
  D11:         else                    // No need to deal with Tail
                  // Read value before CAS
                  // Otherwise, another dequeue might free the next node
  D12:            *pvalue = next.ptr->value
                  // Try to swing Head to the next node
  D13:            if CAS(&Q->Head, head, <next.ptr, head.count+1>)
  D14:               break             // Dequeue is done.  Exit loop
  D15:            endif
  D16:         endif
  D17:      endif
  D18:   endloop
  D19:   free(head.ptr)                // It is safe now to free the old node
  D20:   return TRUE                   // Queue was not empty, dequeue succeeded

Опять же, зачем нужен D5? Правильность или оптимизация? Я не уверен, какую «согласованность» дают эти тесты, поскольку кажется, что они могут стать непоследовательными сразу после успешного выполнения if.

Это похоже на стандартную технику. Может кто-нибудь объяснить мотивы этого? Мне кажется, что намерение состоит в том, чтобы избежать (дорогостоящего) CAS в тех немногих случаях, можно заметить, что он определенно потерпит неудачу, но за счет всегда делать дополнительное чтение, которое не должно быть слишком большим. дешевле самого себя (например, в Java, Q->Tail потребовалось бы быть энергозависимым, поэтому мы бы знали, что мы не просто читаем копию, кэшированную в регистре, но читаем реальную вещь, что будет переводиться при добавлении чтения к забору какой-то), так что я не уверен, что на самом деле здесь происходит ... спасибо.

edit Это было перенесено на Java, точнее в ConcurrentLinkedQueue , например. E7 - это строка 194, а D5 - это строка 212.

Ответы [ 2 ]

4 голосов
/ 05 апреля 2013

Я застрял в этом же вопросе и скептически относился к тому, что это может быть оптимизацией, поэтому я спросил Магеда Майкла, одного из авторов этой статьи. Это его ответ:

E7 и D5 необходимы для правильности.

Следующий случай показывает, почему необходим E7:

  • Поток P читает значение <A,num1> из Q-> Хвост в строке E5

  • Другие потоки изменяют очередь так, что узел A удаляется и, возможно, позже повторно используется в другой очереди (или другой структуре с аналогичной структурой узла) или выделяется потоком, чтобы вставить его в этот та же очередь В любом случае A не находится в этой очереди, и его следующее поле имеет значение <NULL, num2>.

  • В строке E6 P считывает значение <NULL, num2> из A-> next в next.

  • (пропуск линии E7)

  • В строке E8 P находит next.ptr == NULL

  • В строке E9 P выполняет успешный CAS на A-> next, когда находит A->next == <NULL, num2> и устанавливает его в <node,num2+1>.

  • Теперь новый узел неправильно вставлен после A, который не принадлежит этой очереди. Это может также испортить другой, не связанный структура.

В строке E7 P обнаружил бы, что Q-> Tail изменился, и началось бы заново.

Аналогично для D5.

По сути, , если наше чтение из tail.ptr->next заставит нас поверить, что следующий указатель равен нулю (и, таким образом, мы можем записать в узел), мы должны дважды проверить, что этот нуль ссылается до конца текущей очереди. Если узел все еще находится в очереди после того, как мы прочитали ноль, мы можем предположить, что это действительно был конец очереди, и сравнение-и-своп будет (учитывая счетчик) поймать случай, когда что-то случилось с этим узлом после теста в E7 (удаление узла из очереди обязательно повлечет за собой изменение его следующего указателя).

0 голосов
/ 06 октября 2010

Зачем нужен E7?

Это больше для оптимизации.

Рассмотрим два потока, пытающихся поставить в очередь одновременно. Все они попадают в E5, но до того, как поток 1 попадает в E7, поток 2 успешно ставится в очередь. Когда поток 1 доберется до E7, он обнаружит, что t == tail будет ложным, затем попытается снова. Это позволит избежать дорогостоящего CAS. Конечно, это не полное доказательство, потому что E7 может преуспеть до того, как поток 2 поставит в очередь, и в конечном итоге выйдет из строя CAS и все равно придется повторить попытку.

зачем нужен D5

аналогично D5

Опять же, обе функции без E7 и D5 будут работать. Вероятно, был проведен некоторый сравнительный анализ, и было обнаружено, что при умеренной конкуренции двойная проверка увеличивает пропускную способность (это скорее наблюдение, а не факт).

Редактировать:

Я пошел и еще немного прочитал статью об этой очереди. Здесь также проверяется правильность алгоритма без блокировки и меньше состояния структуры данных.

Алгоритм без блокировки является неблокирующим, потому что, если есть незадержанные процессы, пытающиеся выполнить операции над очередь, операция гарантированно завершена в течение конечное время Операция постановки в очередь зацикливается, только если условие в строке Ошибка E7, условие в строке E8 не выполнено или сравнение и своп в строке E9 завершился неудачно. Циклы операции удаления только если условие в строке D5 не выполняется, условие в строке D6 держит (а очередь не пустая), или сравнение и своп в строке D13 не удается. Мы показываем, что алгоритм неблокирует, показывая что процесс выходит за конечное число раз, только если другой процесс завершает операцию в очереди.

http://www.cs.rochester.edu/u/scott/papers/1996_PODC_queues.pdf

...