Advertisement
Guest User

Untitled

a guest
May 23rd, 2018
94
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.70 KB | None | 0 0
  1. ## Универсальная конструкция
  2.  
  3. ### Введение
  4.  
  5. Хотим показать, что если `Consensus-Number(X)⩾n`, то любой объект можно реализовать с гарантией wait-free для `n` потоков с помощью объекта `X` и RW-регистров, причем объект будет линеаризуемым.
  6.  
  7. Например, `Consensus-Number(CAS)=+∞`, значит, любой объект можно сделать с гарантией wait-free.
  8.  
  9. Объект должен работать детерминированно.
  10.  
  11. Объект работает параллельно. Каждый поток хочет выполнить какую-то одну операцию. Но исполняться они должны в одном порядке. Поэтому все потоки должны уметь договориваться, в каком порядке нужно выполнять все предлагаемые ими операции.
  12.  
  13. Этого можно достичь, сделав лог операций (в виде очереди). Каждый поток поддерживает свою копию лога, вызов выполяет следующим образом:
  14. 1. добавляет свой вызов в очередь;
  15. 2. затем смотрит с некоторого «последнего» момента, который он запомнил, и проматывает все операции, пока не доходит до своей (этот момент становится «последним» для локальной копии лога в нашем потоке).
  16.  
  17. Можно делать и умнее (то есть не выполнять все операции из очереди), но это не влияет на соблюдение гарантии wait-free.
  18.  
  19. Заметим, что операции будут линеаризоваться в тот момент, когда они добавляются в очередь: именно в таком порядке они и будут исполняться.
  20.  
  21. Как добавлять в очередь? Если добавления не конкурируют, то все просто. А если конкурируют? Им нужно прийти к консенсусу: какую операцию добавить в очередь первой.
  22.  
  23. ### Универсальная lock-free конструкция
  24.  
  25. В каждом узле очереди будем хранить:
  26. * Invocation — вызов, который нужно выполнить;
  27. * Сonsensus — объект, решающий задачу консенсуса для `n` потоков;
  28. * Seq. number — номер узла в очереди;
  29. * Next — указатель на следующий узел в очереди;
  30.  
  31. Итак, с помощью консенсуса умеем добавлять в конец... Но мы не знаем, где конец находится. В lock-free очереди мы двигали указатель `Tail` с помощью `CAS`, но здесь такая операция отсутствует. Нужно действовать хитрее.
  32.  
  33. `Tail` теперь будет массивом из `n` ячеек, каждая из которых содержит указатель на тот узел в очереди, которые является хвостом для соответствующего данной ячейке потока. Чтобы считать хвост, надо пройти по всем этим указателям и выбрать тот узел, у которого наибольший `Seq. number`. Разумеется, наш хвост не обязательно будет актуальным, поэтому операцию потребуется повторить в неуспешном случае.
  34.  
  35. ```
  36. my = ... // create node with method invocation
  37. while (my->seqnum == 0) {
  38. before = ReadTail()
  39. after = before->consensus.Decide(my)
  40. before->next = after
  41. after->seqnum = before->seqnum + 1
  42. tail[t] = after
  43. }
  44. ```
  45.  
  46. Как это работает? Мы создали какой-то узел, изначально его номер нулевой. Читаем какую-то версию хвоста очереди. Далее выбираем, какой узел будет следующий, с помощью консенсуса: если хвост был актуален, то вернется наш узел, и мы его добавим. Но нас могли опередить, тогда мы добавим узел повторно (ничего не поломается), обновим свой хвост и повторим операцию (так как `seqnum` по-прежнему будет нулевым).
  47.  
  48. Если нас опередили в выборах, но выигравший узел еще не добавили, то его добавим мы. Другой поток проснется (или нет) и добавит его повторно. Эти повторные добавления модифицируют старые узлы очереди, но мы пишем те же значения, что там уже хранились (ведь консенсус вернул одно и то же всем потокам). Если мы выиграли на выборах, но перед его добавлением уснули, то наш узел добавит другой поток, так как ему Consensus по свойству (agreement) вернет наш узел.
  49.  
  50. В данной реализации мы можем проигрывать на выборах сколь угодно много раз. Поэтому гарантия wait-free не соблюдается. Получена универсальная lock-free конструкция.
  51.  
  52. ### Универсальная wait-free конструкция
  53.  
  54. Хотим избежать голодания. Если поток объявил о том, что хочет добавить свой узел в очередь, засыпает, то узел все равно должен добавляться. Идея: просим о помощи.
  55.  
  56. Для этого заводим массив `announce[]` из `n` элементов. В начале добавления положим свой узел в этот массив в свою ячейку. При добавлении сначала подумаем, кому мы можем помочь. Выберем поток, кому помочь, и если его узел еще не добавлен, добавим его. Если же его узел добавлен, то будет добавлять свой узел. Важно: в один и тот же момент (момент определяется номером, который имеет актуальный хвост лога) все потоки пытаются помочь одному и тому же.
  57.  
  58. ```
  59. my = ... // create node with method invocation
  60. announce[t] = my
  61. tail[t] = ReadTail()
  62. while (my->seqnum == 0) {
  63. before = tail[t]
  64. turn = (before->seqnum + 1) % num_threads
  65. if (announce[turn]->seqnum == 0)
  66. candidate = announce[turn]
  67. else
  68. candidate = my
  69. after = before->consensus.Decide(candidate)
  70. before->next = after
  71. after->seqnum = before->seqnum + 1
  72. tail[t] = after
  73. }
  74. tail[t] = my
  75. ```
  76.  
  77. `turn` — номер потока, которому в случае необходимости помогаем. Видно, что мы идем по кругу, то есть для помощи используется механизм round robin.
  78.  
  79. Осталось доказать, что такое добавление соблюдает гарантию wait-free. Действительно, пусть мы анонсировали добавление вершины. Ясно, что среди ближайших `n` вершин найдется такой номер узла, что, оказавшись в некоторый момент хвостом `before`, другой поток нам поможет. Мы тем не менее на каждой итерации цикла сдвигаем `tail` хотя бы на 1. То есть мы выполним не более `n` операций.
  80.  
  81. На самом деле — за `n+1`. Например, пусть `n=3`, `t=1`, `before->seqnum=9`. Тогда понятно, что следующий узел будет 10-й, то есть 1 по модулю `n`, и нам помогут. Но при этом возможно, что _до нашего анонса_ пришел другой поток, который выиграл на выборах. Тогда, конечно, добавят его узел, а для добавления нашего еще `n` итераций придется подождать. Через круг мы точно выиграем, потому что наш анонс уже будет в массиве, и нас никто не опередит. Таким образом, наш узел окажется среди ближайших `n+1` узлов.
  82.  
  83. Итого: пусть мы спали. Если мы проснулись далеко от хвоста, то наш узел уже добавлен, и цикл не продолжится. А проснуться недалеко от хвоста можно не более `O(n)` раз, так как хвост движется вперед каждый раз, а в ближайшие `O(n)` узлов наш узел добавят. Следовательно, каждое добавлеие в очереди выполняется за конечное число шагов, то есть соблюдается гарантия wait-free.
  84.  
  85. #### Очистка памяти
  86.  
  87. Каждый поток имеет свой пул узлов, и он их переиспользует. Это нужно, так как мы хотим соблюсти гарантию wait-free. Если не переиспользовать, то будем зависеть от аллокатора (который, скорее всего, не wait-free). Каждый поток удаляет и переиспользует только «свои» узлы.
  88.  
  89. Заметим, что если мы спим, а хвост продвинулся тем временем сильно вперед, то мы уже не будем смотреть на далекие узлы. Значит, если на какой-то узел точно никто не смотрит, его можно удалять. Спящий поток может обратиться то к `O(n)` ближайшим узлам. Их трогать нельзя. Таким образом, если среди первых `O(n)` потомков узла нет такого, на котором сейчас спит другой поток, то данный узел можно удалить.
  90.  
  91. Итого: пусть мы спали. Если проснулись далеко от хвоста, то наш узел вставили. Мы не посмотрим на далекие узлы. Если проснулись близко к хвосту, то гарантии, что наш узел вставили, еще нет. Тогда удалять нельзя.
  92.  
  93. Каждый поток тем самым «блокирует» удаление `O(n)` узлов. Значит, всего потребуется `O(n²)` узлов.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement