Очередь Майкла и Скотта — различия между версиями
(→Удаление элемента) |
(→Описание проблемы) |
||
Строка 80: | Строка 80: | ||
=== Описание проблемы === | === Описание проблемы === | ||
+ | |||
+ | Рассмотрим ситуацию, при которой два потока <tex>A</tex> и <tex>B</tex> добавляют в очередь элементы <tex>6</tex> и <tex>7</tex>. Рассмотрим следующую последовательность действий: | ||
+ | |||
+ | # Поток <tex>A</tex> добавляет в очередь новую вершину, изменяя <tex>T.next</tex>, но не успевает изменить <tex>T</tex> так, чтобы он указывал на только что добавленную вершину. | ||
+ | # Планировщик операционной системы усыпляет поток <tex>A</tex>. | ||
+ | # Поток <tex>B</tex> собирается добавить новую вершину в очередь, но не может этого сделать, так как постоянно проваливает операцию <tex>CAS(T.next, null, newTail)</tex> (T.next не указывает на <tex>null</tex>, так как поток <tex>A</tex> на шаге <tex>1</tex> добавил в очередь новую вершину, но не передвинул <tex>T</tex>) | ||
+ | # Поток <tex>B</tex> не сможет добавить в очередь новую вершину (а следовательно, завершить операцию <tex>push</tex>), до тех пор, пока планировщик операционной системы не разбудит поток <tex>A</tex>, и поток <tex>A</tex> не завершит добавление (то есть не передвинет <tex>T</tex> на вершину, добавленную на шаге <tex>1</tex>.) | ||
+ | |||
+ | Следовательно, у такой очереди нет гарантии прогресса, мы не получили lock-free алгоритм. | ||
==Примечания== | ==Примечания== |
Версия 19:14, 1 октября 2018
Очередь Майкла и Скотта (Michael-Scott Queue) - алгоритм построения lock-free очереди. Впервые был предложен Maged M. Michael и Michael L. Scot в статье [1].
Содержание
Структура очереди
Очередь моделируется с помощью односвязного списка. Каждый элемент списка (
) содержит ссылку на хранимые в нём данные и указатель на следующий элемент списка (который можно менять атомарно).case class Node(val data: Int, val next: AtomicReference<Node>)
Если узел
является последним в списке, то его указывает на .Сама очередь состоит из двух указателей: на голову
и на хвост , которые можно менять атомарно. Удаление из очереди происходит со стороны головы, добавление - со стороны хвоста.Узел списка, на который указывает
, является фиктивным (dummy). Данные, хранимые в этом узле, не имеют значения. Изначально очередь состоит из одного dummy-элемента, на который указывают и .class Queue dummy = new Node(null, new AtomicReference<Node>(null)) head = new AtomicReference<Node>(dummy) tail = new AtomicReference<Node>(dummy)
// TODO; картинка
Будем поддерживать следующий инвариант: в нашей очереди
указывает на узел, находящийся не правее узла, на который указываетИдея реализации
Удаление элемента
Для удаления элемента необходимо переместить указатель
на следующую в списке вершину.def pop(): Int if (H.next == null): throw new EmptyException() H = H.next return H.data //H - новый фиктивный элемент
Добавление элемента
Создадим новый узел списка, и добавим его в конец очереди.
def push(x: Int): newTail = new Node(x, new AtomicReference<Node>(null)) T.next = newTail //Добавление новой вершины в очередь T = T.next //Изменение хвоста списка
Многопоточная реализация
Будем при всех изменениях указателей на вершины списка использовать
(то есть при изменении , , и )Удаление элемента
Для удаления элемента необходимо переместить указатель
на следующую в списке вершину.def pop(): Int while (true): //Поток пытается в CAS - цикле поменять указатель на H, пока не получится head = H.get() if (head.next == null): throw new EmptyException() newHead = head.next.get() if (CAS(H, head, nextHead)): return newHead.data
Добавление элемента
Создадим новый узел списка, и добавим его в конец очереди.
def push(x: Int): newTail = new Node(x, new AtomicReference<Node>(null)) while (true): //Поток пытается в CAS - цикле поменять T.next, пока не получится tail = T.get() curTail = tail.next if (CAS(curTail, null, newTail)): //Поток пытается добавить элемент в конец очереди break while (true): //Поток пытается в CAS - цикле поменять указатель на T, пока не получится tail = T.get() nextTail = tail.next.get() if (CAS(T, tail, nextTail)): break
При этом мы сталкиваемся со следующей проблемой
Описание проблемы
Рассмотрим ситуацию, при которой два потока
и добавляют в очередь элементы и . Рассмотрим следующую последовательность действий:- Поток добавляет в очередь новую вершину, изменяя , но не успевает изменить так, чтобы он указывал на только что добавленную вершину.
- Планировщик операционной системы усыпляет поток .
- Поток собирается добавить новую вершину в очередь, но не может этого сделать, так как постоянно проваливает операцию (T.next не указывает на , так как поток на шаге добавил в очередь новую вершину, но не передвинул )
- Поток не сможет добавить в очередь новую вершину (а следовательно, завершить операцию ), до тех пор, пока планировщик операционной системы не разбудит поток , и поток не завершит добавление (то есть не передвинет на вершину, добавленную на шаге .)
Следовательно, у такой очереди нет гарантии прогресса, мы не получили lock-free алгоритм.
Примечания
Источники информации
- Maurice Herliny & Nir Shavit - The Art of Multiprocessor programming, стр 230