SlideShare a Scribd company logo
Параллельное программирование


       Роман Елизаров, 2009
      elizarov@devexperts.com
Взаимное исключение


• Взаимное исключение (Mutual Exclusion)
• Отсутствие блокировки (Freedom from Deadlock)
• Отсутствие голодания (Freedom from Starvation)

  run() {
    int i = getCurrentThreadID();
    while (true) {
      nonCriticalSection();
      lock(i);
      criticalSection();
      unlock(i);
    }
  }
Взаимное исключение, попытка 1

 boolean want[2];

 lock(int i) {
   want[i] = true;
   while (want[1 – i]); // wait
 }

 unlock(int i) {
   want[i] = false;
 }
Взаимное исключение, попытка 2

 int victim;

 lock(int i) {
   victim = i;
   while (victim == i); // wait
 }

 unlock(int i) {
 }
Взаимное исключение, алгоритм Петерсона

 boolean want[2];
 int victim;

 lock(int i) {
   want[i] = true;
   victim = i;
   while (want[1 – i] && victim == i); // wait
 }

 unlock(int i) {
   want[i] = false;
 }
Взаимное исключение, алгоритм Петерсона
для N потоков
 int level[N];
 int victim[N];

 lock(int i) {
   for (int j = 1; j < N; j++) {
     level[i] = j;
     victim[j] = i;
     while exists k != i :
       level[k] >= j && victim[j] == i; // wait
 }

 unlock(int i) {
   level[i] = 0;
 }
Честность


• Отсутствие голодания
• Линейное ожидание, квадратичное ожидание и т.п.
• Первым пришел, первым обслужен (FCFS – First Come First
  Served)


 lock() {
     DoorwaySection(); // wait-free code
     WaitingSection();
 }
Взаимное исключение, алгоритм Лампорта
(алгоритм булочника – вариант 1)
 boolean want[N]; // init with false
 Label label[N]; // init with 0

 lock(int i) {
   want[i] = true;
   label[i] = max(label[0], …, label[N-1]) + 1;
   while exists k != i :
     want[k] && (label[k], k) < (label[i], i);
 }

 unlock(int i) {
   want[i] = false;
 }
Взаимное исключение, алгоритм Лампорта
(алгоритм булочника – вариант 2)
 boolean choosing[N]; // init with false
 Label label[N];      // init with inf

 lock(int i) {
   choosing[i] = true;
   label[i] = max(label[0], …, label[N-1]) + 1;
   choosing[i] = false;
   while exists k != i :
     choosing[k] || (label[k], k) < (label[i], i);
 }

 unlock(int i) {
   label[i] = inf;
 }
Разделяемые объекты


• Корректность реализации объекта
   - Тихая согласованность (Quiescent consistency)
   - Последовательная согласованность (Sequential consistency)
   - Линеаризуемость (Linearizability)
• Прогресс
   - Без помех (Obstruction-free)
   - Без блокировок (Lock-free)
   - Без ожидания (Wait-free)
Регистры


• Разделяемые регистры – базовый объект для общения
  потоков между собой


 interface Register<T> {
   T read();
   void write(T val);
 }
Классификация регистров


• Безопасные (safe), регулярные (regular), атомарные (atomic)
• Один читатель, много читателей (SR, MR)
• Один писатель, много писателей (SW, MW)
• Булевские значение, множественные значения

• Самый примитивный регистр – Safe SRSW Boolean register
• Самый сложный регистр – Atomic MRMW M-Valued register
Построение регистров


• Будем строить более сложные регистры из более простых без
  ожиданий (wait-free образом).
   - Safe SRSW Boolean register
   - Regular SRSW Boolean register
   - Regular SRSW M-Valued register
   - Atomic SRSW M-Valued register
   - Atomic MRSW M-Valued register
   - Atomic MRMW M-Valued register
Атомарный снимок состояния N регистров


• Набор SW атомарных регистров (по регистру на поток)
• Любой поток может вызвать scan() чтобы получить снимок
  состояния всех регистров
• Методы должны быть атомарными (линеаризуемыми)

 interface Snapshot<T> {
   void update(int i, T val);
   T[] scan();
 }
Атомарный снимок состояния N регистров,
без блокировок (lock free)
 // каждый регистр хранит версию “version”
 (T val, Label version) register[N];

 void update(int i, T val) { // wait-free
   register[i] = (val, register[i].version+1);
 }

 T[] scan() { // obstruction-free
   (T, Label)[] old = copyOf(register); // with loop
   while (true) {
     (T, Label)[] cur = copyOf(register);
     if (equal(old, cur)) return cur.val;
                     else old = cur;
   }
 }
Атомарный снимок состояния N регистров,
без ожидания (wait-free) – update
 // каждый регистр так же хранит копию снимка “snap”
 (T val, Label version, T[] snap) register[N];

 void update(int i, T val) { // wait-free
   T[] snap = scan();
   register[i] = (val, register[i].version+1, snap);
 }
Атомарный снимок состояния N регистров,
без ожидания (wait-free) – scan
 T[] scan() { // wait-free, O(N^2) time
   (T, Label, T[])[] old = copyOf(register);
   boolean updated[N];
   loop: while (true) {
     (T, Label, T[])[] cur = copyOf(register);
     for (int j = 0; j < N; j++)
       if (cur[j].version != old[j].version)
         if (updated[j]) return cur[j].snap;
         else {
           updated[j] = true;
           old = cur; continue loop;
         }
     return cur.val;
   }
 }
Консенсус


• Согласованность (consistent): все потоки должны вернуть одно
  и то же значение из метода decide
• Обоснованность (valid): возвращенное значение было
  входным значением какого-то из потоков

 interface Consensus<T> {
   T decide(T val);
 }
Консенсусное число


• Если с помощью класса объектов C можно реализовать
  консенсусный протокол без ожидания (wait-free) для N потоков
  (и не больше), то говорят что у класса C консенсусное число
  равно N.
• ТЕОРЕМА: Атомарные регистры имеют консенсусное число 1.
   - Т.е. с помощью атомарных регистров даже 2 потока не могут
     придти к консенсусу без ожидания (докажем от противного) для
     для 2-х возможных значений при T = {0, 1}
   - С ожиданием задача решается очевидно (с помощью любого
     алгоритма взаимного исключения).
Определения и леммы для любых классов
объектов

• Определения и концепции:
   - Рассматриваем дерево состояния, листья – конечные состояния
     помеченные 0 или 1 (в зависимости от значения консенсуса).
   - x-валентное состояние системы (x = 0,1) – консенсус во всех
     нижестоящих листьях будет x.
   - Бивалентное состояние – возможен консенсус как 0 так и 1.
   - Критическое состояние – такое бивалентное состояние, все дети
     которого одновалентны.
• ЛЕММА: Существует начальное бивалентное состояние.
• ЛЕММА: Существует критическое состояние.
Доказательство для атомарных регистров


• Рассмотрим возможные пары операций в критическом
  состоянии:
   - Операции над разными регистрами – коммутируют.
   - Два чтения – коммутируют.
   - любая операция + Запись – состояние пишущего потока не
     зависит от порядка операций.
Read-Modify-Write регистры


• Для функции или класса функций F(args): T -> T
   - getAndSet (exchange), getAndIncrement, getAndAdd и т.п.
   - get (read) это тоже [тривиальная] RMW операция для F == id.


 сlass RMWRegister<T> {
   private T val;
   T getAndF(args…) atomic {
     T old = val;
     val = F(T, args);
     return old;
   }
 }
Нетривиальные RMW регистры


• Консенсусное число нетривиального RMW регистра >= 2.
   - Нужно чтобы была хотя бы одна «подвижная» точка функции F,
     например F(v0) = v1 != v0.
 T proposed[2];
 RMWRegister rmw; // начальное значение v0

 T decide(T val) {
   int i = myThreadId(); // i = 0,1 – номер потока
   proposed[i] = val;
   if (rmw.getAndF() == v0) return proposed[i];
                       else return proposed[1 – i];
 }
Common2 RMW регистры


• Определения
  - F1 и F2 коммутируют если F1(F2(x)) == F2(F1(x))
  - F1 перезаписывает F2 если F1(F2(x)) == F1(x)
  - Класс С RMW регистров принадлежит Common2, если любая
    пара функций либо коммутирует, либо одна из функций
    перезаписывает другую.
• ТЕОРЕМА: Нетривиальный класс Common2 RMW регистров
  имеет консенсусное число 2.
  - Третий поток не может отличить глобальное состоянием при
    изменения порядка выполнения коммутирующих или
    перезаписывающих операций в критическом состоянии.
Универсальные объекты


• Объект с консенсусный числом бесконечность называется
  универсальным объектом.
   - По определению, с помощью него можно реализовать
     консенсусный протокол для любого числа потоков.
• Пример: compareAndSet aka testAndSet (возвращает boolean),
  compareAndExchange (возвращает старое значение – RMW)

 private T x;
 boolean compareAndSet(T val, T expected) atomic {
   if (x == expected) { x = val; return true; }
                 else return false;
 }
compareAndSet (CAS) и консенсус

// реализация консенсусного протокола через CAS+GET
T decide(T val) {
  if (compareAndSet(val, INITIAL))
          return val;
    else return get();
}

// реализация консенсусного протокола через CMPXCHG
T decide(T val) {
  T old = compareAndExchange(val, INITIAL);
  if (old == INITIAL)
          return val;
    else return old;
}
Универсальность консенсуса


• ТЕОРЕМА: Любой последовательный объект можно
  реализовать без ожидания (wait-free) для N потоков используя
  консенсусный протокол для N объектов.
   - Следствие1: С помощью любого класса объектов с консенсусным
     числом N можно реализовать любой объект с консенсусным
     числом <= N.
   - Следствие2: С помощью универсального объекта можно
     реализовать любой объект.
Иерархия объектов


 Консенсусное   Объект
 число
 1              Атомарные регистры, снимок состояния нескольких
                регистров
 2              getAndSet (атомарный обмен), getAndAdd, очередь,
                стэк
 m              Атомарная запись m регистров из m(m+1)/2
                регистров
 ∞              compareAndSet, LoadLinked/StoreConditional
Многопоточные (Thread-Safe) объекты
(алгоритмы и структуры данных) на практике

• Многопоточный объект включает в себя синхронизацию
  потоков (блокирующую или не блокирующую), которая
  позволяет его использовать из нескольких потоков
  одновременно без дополнительной внешней синхронизации
   - Специфицируется через последовательное поведение
   - По умолчанию требуется линеаризуемость операций (более
     слабые формы согласованности – редко)
   - Редко удается реализовать все операции без ожидания (wait-
     free). Часто приходится идти на компромиссные решения.
• ВНИМАНИЕ: Пока пишем псевдокод. Доведение его до
  реального кода будем обсуждать отдельно.
Разные подходы к синхронизации потоков при
работе с общей структурой данных

• Типы синхронизации:
   - Грубая (Coarse-grained) синхронизация
   - Тонкая (Fine-grained) синхронизация
   - Оптимистичная (Optimistic) синхронизация
   - Ленивая (Lazy) синхронизация
   - Неблокирующая (Nonblocking) синхронизация
• Проще всего для списочных структур данных (с них и начнем),
  хотя на практике массивы работают существенно быстрей
Пример: Множество на основе односвязного
списка

• ИНВАРИАНТ: node.key < node.next.key


 class Node {
     int key;
     T item;
     Node next;
 }

 // Пустой список состоит их 2-х граничных элементов
 Node head = new Node(Integer.MIN_VALUE, null);
 head.next = new Node(Integer.MAX_VALUE, null);
Грубая синхронизация


• Обеспечиваем взаимное исключение всех операций через
  общий Mutex lock.
Грубая синхронизация: поиск

 boolean contains(int key) {
     lock.lock();
     try {
         Node curr = head;
         while (curr.key < key) {
             curr = curr.next;
         }
         return key == curr.key;
     } finally { lock.unlock(); }
 }
Грубая синхронизация: добавление

 boolean add(int key, T item) {
     lock.lock();
     try {
         Node pred = head, curr = pred.next;
         while (curr.key < key) {
             pred = curr; curr = curr.next;
         }
         if (key == curr.key) return false; else {
             Node = new Node(key, item);
             node.next = curr; pred.next = node;
             return true;
         }
     } finally { lock.unlock(); }
 }
Грубая синхронизация: удаление

 boolean remove(int key, T item) {
     lock.lock();
     try {
         Node pred = head, curr = pred.next;
         while (curr.key < key) {
             pred = curr; curr = curr.next;
         }
         if (key == curr.key) {
             pred.next = curr.next;
             return true;
         else {
             return false;
         }
     } finally { lock.unlock(); }
 }
Тонкая синхронизация


• Обеспечиваем синхронизацию через взаимное исключение на
  каждом элементе.
• При любых операциях одновременно удерживаем блокировку
  текущего и предыдущего элемента (чтобы не утерять
  инвариант pred.next == curr).
Тонкая синхронизация: добавление

 Node pred = head; pred.lock();
 Node curr = pred.next; curr.lock();
 try {
     while (curr.key < key) {
         pred.unlock(); pred = curr;
         curr = curr.next; curr.lock();
     }
     if (key == curr.key) return false; else {
         Node = new Node(key, item);
         node.next = curr; pred.next = node;
         return true;
     }
 } finally { curr.unlock(); pred.unlock(); }
Оптимистичная синхронизация


• Ищем элемент без синхронизации (оптимистично предполагая
  что никто не помешает), но перепроверяем с синхронизацией
   - Если перепроверка обломалась, то начинаем операцию заново
• Имеет смысл только если обход структуры дешев и быстр, а
  обход с синхронизацией медленный и дорогой
Оптимистичная синхронизация: поиск

 retry: while(true) {
     Node pred = head, curr = pred.next;
     while (curr.key < key) {
         pred = curr; curr = curr.next;
     }
     pred.lock(); curr.lock();
     try {
         if (!validate(pred, curr)) continue retry;
         return curr.key == key;
     } finally { curr.unlock(); pred.unlock(); }
 }
Оптимистичная синхронизация: валидация

 boolean validate(Node pred, Node curr) {
     Node node = head;
     while (node.key <= pred.key) {
         if (node == pred)
             return pred.next == curr;
         node = node.next;
     }
     return false;
 }
Оптимистичная синхронизация: добавление

 retry: while(true) {
     Node pred = head, curr = pred.next;
     while (curr.key < key) {
         pred = curr; curr = curr.next;
     }
     pred.lock(); curr.lock();
     try {
         if (!validate(pred, curr)) continue retry;
         if (curr.key == key) return false; else {
             Node = new Node(key, item);
             node.next = curr; pred.next = node;
             return true;
         }
     } finally { curr.unlock(); pred.unlock(); }
 }
Оптимистичная синхронизация: удаление

 retry: while(true) {
     Node pred = head, curr = pred.next;
     while (curr.key < key) {
         pred = curr; curr = curr.next;
     }
     pred.lock(); curr.lock();
     try {
         if (!validate(pred, curr)) continue retry;
         if (curr.key == key) {
             pred.next = curr.next;
             return true;
         } else return false;
     } finally { curr.unlock(); pred.unlock(); }
 }
Ленивая синхронизация


• Добавляем в Node поле boolean marked.
• Удаление в 2 фазы:
   - node.marked = true; // Логическое удаление
   - Физическое удаление из списка
• Инвариант: Все непомеченные (неудаленные) элементы
  всегда в списке
• Результат:
   - Для валидации не надо просматривать список (только проверить
     что элементы не удалены логически и pred.next == curr). В
     остальном, код добавление идентичен оптимистичному.
   - Поиск элемента в списке можно делать без ожидания (wait-free)
Ленивая синхронизация: удаление

 retry: while(true) {
     Node pred = head, curr = pred.next;
     while (curr.key < key) {
         pred = curr; curr = curr.next;
     }
     pred.lock(); curr.lock();
     try {
         if (!validate(pred, curr)) continue retry;
         if (curr.key == key) {
             curr.marked = true;     точка линеаризации
             pred.next = curr.next;
             return true;
         } else return false;
     } finally { curr.unlock(); pred.unlock(); }
 }
Ленивая синхронизация: валидация

 boolean validate(Node pred, Node curr) {
     return !pred.marked &&
            !curr.marked &&
            pred.next == curr;
 }
Ленивая синхронизация: поиск (wait-free!)

 boolean contains(int key) {
     Node curr = head;
     while (curr.key < key) {
         curr = curr.next;
     }
     return key == curr.key && !curr.marked;
 }

                            точка линеаризации
                             успешного поиска
Неблокирующая синхронизация


• Простое использование Compare-And-Set (CAS) не помогает –
  удаления двух соседних элементов будут конфликтовать.
• Объединим next и marked в одну переменную {next,marked},
  которую будем атомарно менять используя CAS
   - Каждая операция модификации будет выполнятся одним
     успешным CAS-ом.
   - Успешное выполнение CAS-а является точкой линеаризации.
• При выполнении операции удаления или добавления будем
  пытаться произвести физическое удаление
   - Добавление и удаление будут работать без блокировки (lock-free)
   - Поиск элемента будет работать без ожидания (wait-free)
Неблокирующая синхронизация: добавление

  retry: while (true) {
       Node pred, curr; {pred,curr} = find(key);
       if (curr.key == key) return false; else {
            Node = new Node(key, item);
            node.{next,marked} = {curr,false};
линеаризация   if (CAS(pred.{next,marked}, {curr,false},
                                           {next,false})
                   return true;
       }
  }
Неблокирующая синхронизация: поиск окна

 {Node,Node} find(int key) {
  retry: while(true) {
      Node pred = head, curr = pred.next, succ;
      while (true) {
          {succ,boolean cmk} = curr.{next,marked};
          if (cmk) { // Если curr логически удален
              if (!CAS(pred.{next,marked},
                      {curr,false}, {succ,false}))
                  continue retry;
              curr = succ;
          } else {
              if (curr.key >= key) return {pred,curr};
              pred = curr; curr = succ;
          }
      }
  }
Неблокирующая синхронизация: удаление

 retry: while (true) {
      Node pred, curr; {pred,curr} = find(key);
      if (curr.key != key) return false; else {
           Node succ = curr.next;
линеаризация  if (!CAS(curr.{next,marked}, {next,false},
                                             {next,true})
                  continue retry;
           // оптимизация – попытаемся физ. удалить
           CAS(pred.{next,marked}, {curr,false}
                                    {succ,false});
           return true;
      }
 }
Универсальное построение без блокировок с
использованием CAS

• Вся структура данных представляется как указатель на
  объект, содержимое которого никогда не меняется.
• Любые операции чтения работают без ожидания.
• Любые операции модификации создают полную копию
  структуры, меняют её, из пытаются подменить указать на неё с
  помощью одного Compare-And-Set (CAS).
   - В случае ошибки CAS – повтор.
• Частный случай этого подхода: вся структура данных влезает
  в одно машинное слово, например счетчик.
Атомарный счетчик

 int counter;

 int getAndIncrement(int increment) {
     retry: while(true) {
         int old = counter;
         int updated = old + increment;
         if (CAS(counter, old, updated))
             return old;
     }
 }
Работа с древовидными структурами без
блокировок

• Структура представлена в виде дерева
• Тогда операции изменения можно реализовать в виде одного
  CAS, заменяющего указатель на root дерева.
   - Неизменившуюся часть дерева можно использовать в новой
     версии дерева, т.е. не нужно копировать всю структуру данных.
• Частный случай этого подхода: LIFO стек
 class Node {
     T item;
     Node next;
 }
 // Пустой стек это указатель на null
 Node top = null;
Операции с LIFO стеком

 void push(T item) {
     retry: while(true) {
         Node node = new Node(item, top);
         if (CAS(top, node.next, node))     линеаризация
             return;
     }
 }
 T pop() {
     retry: while(true) {
         Node node = top;
         if (node == null) throw new EmptyStack();
         if (CAS(top, node, node.next))    линеаризация
             return node.item;
     }
 }
FIFO очередь без блокировок (lock-free)


• Так же односвязный список, но два указателя: head и tail.
• Алгоритм придумали Michael & Scott в 1996 году.
 class Node {
     T item;
     Node next;
 }

 // Пустой список состоит их одного элемента-заглушки
 Node head = new Node(null);
 Node tail = head;
Добавление в очередь

 void enqueue(T item) {
      Node node = new Node(item);
      retry: while(true) {
           Node last = tail, next = last.next;
           if (next == null) {
линеаризация   if (!CAS(last.next, null, node))
                   continue retry;
               // оптимизация – сами переставляем tail
               CAS(tail, last, node);
               return;
           }
           // Помогаем другим операциям enqueue с tail
           CAS(tail, last, next);
      }
 }
Удаление из очереди

 T dequeue() {
      retry: while(true) {
           Node first = head, last = tail,
                next = first.next;
           if (first == last) {
               if (next == null) throw new EmptyQueue();
               // Помогаем операциям enqueue с tail
               CAS(tail, last, next);
           } else {
линеаризация   if (CAS(head, first, next))
                    return next.item;
           }
      }
 }
Работа без GC, проблема ABA


• Память освобождается явно через “free” с добавлением в
  список свободной памяти:
   - Содержимое ячейки может меняться произвольным образом,
     пока на неё удерживается указатель
      • решение – дополнительные перепроверки
   - CAS может ошибочно отработать из-за проблемы ABA
      • решение – добавление версии к указателю
• Альтернативное решение – свой GC
Очереди/стеки на массивах


• Структуры на массивах быстрей работают на практике из-за
  локальности доступа к данным
• Очевидные решения не работают
   - Стек на массиве не работает
   - Очередь работает только при проходе по памяти один раз (можно
     развернуть очередь со списками для увеличения
     быстродействия)
• Неочевидные решения
   - Дек без помех (Obstruction-free Deque)
   - DCAS/CASn (Обновление нескольких слов одновременно)
   - Используя дескрипторы операций (универсальная конструкция)
Дек без помех


• Каждый элемента массива должен содержать элемент и
  версию, которые мы будем атомарно обновлять CAS-ом
• Пустые элементы будут заполнены правыми и левыми нулями
  (RN и LN)
• Указатели на правый и левый край будут храниться
  «приблизительно» и подбираться перед выполнением
  операций с помощью оракула (rightOracle и leftOracle)
  // массив на MAX элементов (0..MAX-1)
  {T item, int ver} a[MAX];
  int left, right; // прибл. указатель на LN и RN
Оракул для поиска правого края

 // Должен находить такое место что:
 //    a[k] == RN && a[k – 1] != RN
 // Должен корректно работать «без помех»

 int rightOracle() {
     int k = right; // только для оптимизации
     while (a[k] != RN) k++;
     while (a[k-1] == RN) k--;
     right = k; // запомнили для оптимизации
     return k;
 }
Добавление справа

 void rightPush(T item) {
  retry: while(true) {
      int k = rightOracle();
      {T item,int ver} prev = a[k-1], cur = a[k];
      if (prev.item == RN || cur.item != RN) continue;
      if (k == MAX-1) throw new FullDeque();
      if (CAS(a[k-1], prev, {prev.item,prev.ver+1} &&
          CAS(a[k], cur, {item,cur.ver+1})
          return; // успешно закончили
 }
Удаление справа

 T rightPop() {
  retry: while(true) {
      int k = oracleRight();
      {T item,int ver} cur = a[k-1], next = a[k];
      if (cur.item == RN || next.item != RN) continue;
      if (cur.item == LN) throw new EmptyDeque();
      if (CAS(a[k], next, {RN,next.ver+1} &&
          CAS(a[k-1], cur, {RN,cur.ver+1})
          return cur.item; // успешно закончили
 }
Хэш-таблицы


• Два основных способа разрешения коллизий
   - Прямая адресация: каждая ячейка хранит список элементов
      • Естественный параллелизм, легко делать раздельные
        блокировки/нарезку блокировок (lock striping)
      • Применяя алгоритмы работы со списками/множествами можно
        сделать реализацию без блокировок
   - Открытая адресация: ищем в других ячейках
      • На практике быстрей искать в соседних элементах, но требует
        хэш-функции хорошего качества
      • Так же возможна реализация без блокировок (занимаем
        ячейку через CAS), но требует специальной техники удаления
• Изменение размера хэш-таблицы (rehash)

More Related Content

PDF
ПВТ - осень 2014 - Лекция 3 - Стандарт POSIX Threads
PDF
ПВТ - осень 2014 - Лекция 6 - Атомарные операции. Внеочередное выполнение инс...
PDF
Кулагин И.И., Пазников А.А., Курносов М.Г. Оптимизация информационных обменов...
PDF
ПВТ - весна 2015 - Лекция 3. Реентерабельность. Сигналы. Локальные данные пот...
PDF
ПВТ - весна 2015 - Лекция 8. Многопоточное программирование без использования...
PDF
ПВТ - весна 2015 - Лекция 6. Разработка параллельных структур данных на основ...
PDF
ПВТ - весна 2015 - Лекция 2. POSIX Threads. Основные понятия многопоточного п...
PPTX
Reactive extensions
ПВТ - осень 2014 - Лекция 3 - Стандарт POSIX Threads
ПВТ - осень 2014 - Лекция 6 - Атомарные операции. Внеочередное выполнение инс...
Кулагин И.И., Пазников А.А., Курносов М.Г. Оптимизация информационных обменов...
ПВТ - весна 2015 - Лекция 3. Реентерабельность. Сигналы. Локальные данные пот...
ПВТ - весна 2015 - Лекция 8. Многопоточное программирование без использования...
ПВТ - весна 2015 - Лекция 6. Разработка параллельных структур данных на основ...
ПВТ - весна 2015 - Лекция 2. POSIX Threads. Основные понятия многопоточного п...
Reactive extensions

What's hot (20)

PDF
ПВТ - осень 2014 - Лекция 5 - Многопоточное программирование в языке С++. Р...
PDF
Объектно-ориентированное программирование. Лекция 5 и 6
PDF
Zagursky
PDF
ПВТ - весна 2015 - Лекция 1. Актуальность параллельных вычислений. Анализ пар...
PDF
1 встреча — Параллельное программирование (А. Свириденков)
ODP
PPTX
Multiprocessor Programming Intro (lecture 2)
PDF
ПВТ - весна 2015 - Лекция 7. Модель памяти С++. Внеочередное выполнение инстр...
PDF
Введение в Clojure (Никита Прокопов)
PDF
Модель памяти C++ - Андрей Янковский, Яндекс
PDF
ПВТ - осень 2014 - Лекция 4 - Стандарт POSIX Threads. Реентерабельность. Сигн...
PDF
Cpp0x Introduction
PDF
Юрий Ефимочев, Компилируемые в реальном времени DSL для С++
DOC
020
PDF
Дмитрий Кашицын, Троллейбус из буханки: алиасинг и векторизация в LLVM
PDF
ПВТ - весна 2015 - Лекция 5. Многопоточное программирование в С++. Синхрониза...
PPTX
Multiprocessor Programming Intro (lecture 3)
PDF
Многопоточные Алгоритмы (для BitByte 2014)
PDF
Опыт применения активных объектов во встраиваемых системах. Архитектурные асп...
PDF
Probabilistic Verification in Computational Systems Design
ПВТ - осень 2014 - Лекция 5 - Многопоточное программирование в языке С++. Р...
Объектно-ориентированное программирование. Лекция 5 и 6
Zagursky
ПВТ - весна 2015 - Лекция 1. Актуальность параллельных вычислений. Анализ пар...
1 встреча — Параллельное программирование (А. Свириденков)
Multiprocessor Programming Intro (lecture 2)
ПВТ - весна 2015 - Лекция 7. Модель памяти С++. Внеочередное выполнение инстр...
Введение в Clojure (Никита Прокопов)
Модель памяти C++ - Андрей Янковский, Яндекс
ПВТ - осень 2014 - Лекция 4 - Стандарт POSIX Threads. Реентерабельность. Сигн...
Cpp0x Introduction
Юрий Ефимочев, Компилируемые в реальном времени DSL для С++
020
Дмитрий Кашицын, Троллейбус из буханки: алиасинг и векторизация в LLVM
ПВТ - весна 2015 - Лекция 5. Многопоточное программирование в С++. Синхрониза...
Multiprocessor Programming Intro (lecture 3)
Многопоточные Алгоритмы (для BitByte 2014)
Опыт применения активных объектов во встраиваемых системах. Архитектурные асп...
Probabilistic Verification in Computational Systems Design
Ad

Viewers also liked (20)

PPTX
Digital matters event information
ODP
bedrijfspresentatie
DOCX
Universidad Autonoma De Guerrero
PDF
Athletic Business 2013 - San Diego - Technology's Approaching Impact on the ...
PPTX
Day after day...
PDF
Concorso 65 Coadiutori Banca d'Italia - Bando
PPTX
American rap presentation
PPS
El Mundo En 100 Personas
PPTX
2013.05 - LDOW 2013 @ WWW 2013
PPTX
La placa madre sesión 05
PDF
Namasmaran Bestseller On Superliving Dr. Shriniwas Kashalikar
PPTX
Embracing the New Social Media
PPTX
Doctoral Examination at the Karlsruhe Institute of Technology (08.07.2016)
PDF
20100321 virtualization igotti_lecture08
PPS
Que Nos Paso
DOCX
1. cover ppl
DOCX
Mcdi u1 ea_lula
PPTX
Kennedy Power PointChapter 49
PPT
Glenthorne Media Academy
Digital matters event information
bedrijfspresentatie
Universidad Autonoma De Guerrero
Athletic Business 2013 - San Diego - Technology's Approaching Impact on the ...
Day after day...
Concorso 65 Coadiutori Banca d'Italia - Bando
American rap presentation
El Mundo En 100 Personas
2013.05 - LDOW 2013 @ WWW 2013
La placa madre sesión 05
Namasmaran Bestseller On Superliving Dr. Shriniwas Kashalikar
Embracing the New Social Media
Doctoral Examination at the Karlsruhe Institute of Technology (08.07.2016)
20100321 virtualization igotti_lecture08
Que Nos Paso
1. cover ppl
Mcdi u1 ea_lula
Kennedy Power PointChapter 49
Glenthorne Media Academy
Ad

Similar to 20090222 parallel programming_lecture01-07 (20)

PDF
C++ осень 2012 лекция 5
PDF
Дмитрий Прокопцев — R-ссылки в С++11
PDF
Лекция 8. Intel Threading Building Blocks
PPT
верификация
PPS
javascript
PPS
javascript_part1
PPTX
Базовые операторы Java
PDF
Step cpp0201
PDF
PDF
Павел Сушин «Асинхронное программирование на С++: callbacks, futures, fibers»
PPTX
Deep Dive C# by Sergey Teplyakov
PPT
9. java lecture library
PDF
Atomics, CAS and Nonblocking algorithms
PPTX
Хранимые процедуры в NoSQL СУБД на примере Tarantool / Денис Линник (Mail.Ru)
PPT
язык програмирования
PDF
Лекция 8: Многопоточное программирование: Intel Threading Building Blocks
PDF
Погружение в Dart
PDF
Евгений Крутько — Опыт внедрения технологий параллельных вычислений для повыш...
PDF
Антон Потапов — С++ контейнеры и многопоточность: вместе или врозь?
PDF
Объектно-Ориентированное Программирование на C++, Лекции 3 и 4
C++ осень 2012 лекция 5
Дмитрий Прокопцев — R-ссылки в С++11
Лекция 8. Intel Threading Building Blocks
верификация
javascript
javascript_part1
Базовые операторы Java
Step cpp0201
Павел Сушин «Асинхронное программирование на С++: callbacks, futures, fibers»
Deep Dive C# by Sergey Teplyakov
9. java lecture library
Atomics, CAS and Nonblocking algorithms
Хранимые процедуры в NoSQL СУБД на примере Tarantool / Денис Линник (Mail.Ru)
язык програмирования
Лекция 8: Многопоточное программирование: Intel Threading Building Blocks
Погружение в Dart
Евгений Крутько — Опыт внедрения технологий параллельных вычислений для повыш...
Антон Потапов — С++ контейнеры и многопоточность: вместе или врозь?
Объектно-Ориентированное Программирование на C++, Лекции 3 и 4

More from Computer Science Club (20)

PDF
20141223 kuznetsov distributed
PDF
Computer Vision
PDF
20140531 serebryany lecture01_fantastic_cpp_bugs
PDF
20140531 serebryany lecture02_find_scary_cpp_bugs
PDF
20140531 serebryany lecture01_fantastic_cpp_bugs
PDF
20140511 parallel programming_kalishenko_lecture12
PDF
20140427 parallel programming_zlobin_lecture11
PDF
20140420 parallel programming_kalishenko_lecture10
PDF
20140413 parallel programming_kalishenko_lecture09
PDF
20140329 graph drawing_dainiak_lecture02
PDF
20140329 graph drawing_dainiak_lecture01
PDF
20140310 parallel programming_kalishenko_lecture03-04
PDF
20140223-SuffixTrees-lecture01-03
PDF
20140216 parallel programming_kalishenko_lecture01
PDF
20131106 h10 lecture6_matiyasevich
PDF
20131027 h10 lecture5_matiyasevich
PDF
20131027 h10 lecture5_matiyasevich
PDF
20131013 h10 lecture4_matiyasevich
PDF
20131006 h10 lecture3_matiyasevich
PDF
20131006 h10 lecture3_matiyasevich
20141223 kuznetsov distributed
Computer Vision
20140531 serebryany lecture01_fantastic_cpp_bugs
20140531 serebryany lecture02_find_scary_cpp_bugs
20140531 serebryany lecture01_fantastic_cpp_bugs
20140511 parallel programming_kalishenko_lecture12
20140427 parallel programming_zlobin_lecture11
20140420 parallel programming_kalishenko_lecture10
20140413 parallel programming_kalishenko_lecture09
20140329 graph drawing_dainiak_lecture02
20140329 graph drawing_dainiak_lecture01
20140310 parallel programming_kalishenko_lecture03-04
20140223-SuffixTrees-lecture01-03
20140216 parallel programming_kalishenko_lecture01
20131106 h10 lecture6_matiyasevich
20131027 h10 lecture5_matiyasevich
20131027 h10 lecture5_matiyasevich
20131013 h10 lecture4_matiyasevich
20131006 h10 lecture3_matiyasevich
20131006 h10 lecture3_matiyasevich

20090222 parallel programming_lecture01-07

  • 1. Параллельное программирование Роман Елизаров, 2009 elizarov@devexperts.com
  • 2. Взаимное исключение • Взаимное исключение (Mutual Exclusion) • Отсутствие блокировки (Freedom from Deadlock) • Отсутствие голодания (Freedom from Starvation) run() { int i = getCurrentThreadID(); while (true) { nonCriticalSection(); lock(i); criticalSection(); unlock(i); } }
  • 3. Взаимное исключение, попытка 1 boolean want[2]; lock(int i) { want[i] = true; while (want[1 – i]); // wait } unlock(int i) { want[i] = false; }
  • 4. Взаимное исключение, попытка 2 int victim; lock(int i) { victim = i; while (victim == i); // wait } unlock(int i) { }
  • 5. Взаимное исключение, алгоритм Петерсона boolean want[2]; int victim; lock(int i) { want[i] = true; victim = i; while (want[1 – i] && victim == i); // wait } unlock(int i) { want[i] = false; }
  • 6. Взаимное исключение, алгоритм Петерсона для N потоков int level[N]; int victim[N]; lock(int i) { for (int j = 1; j < N; j++) { level[i] = j; victim[j] = i; while exists k != i : level[k] >= j && victim[j] == i; // wait } unlock(int i) { level[i] = 0; }
  • 7. Честность • Отсутствие голодания • Линейное ожидание, квадратичное ожидание и т.п. • Первым пришел, первым обслужен (FCFS – First Come First Served) lock() { DoorwaySection(); // wait-free code WaitingSection(); }
  • 8. Взаимное исключение, алгоритм Лампорта (алгоритм булочника – вариант 1) boolean want[N]; // init with false Label label[N]; // init with 0 lock(int i) { want[i] = true; label[i] = max(label[0], …, label[N-1]) + 1; while exists k != i : want[k] && (label[k], k) < (label[i], i); } unlock(int i) { want[i] = false; }
  • 9. Взаимное исключение, алгоритм Лампорта (алгоритм булочника – вариант 2) boolean choosing[N]; // init with false Label label[N]; // init with inf lock(int i) { choosing[i] = true; label[i] = max(label[0], …, label[N-1]) + 1; choosing[i] = false; while exists k != i : choosing[k] || (label[k], k) < (label[i], i); } unlock(int i) { label[i] = inf; }
  • 10. Разделяемые объекты • Корректность реализации объекта - Тихая согласованность (Quiescent consistency) - Последовательная согласованность (Sequential consistency) - Линеаризуемость (Linearizability) • Прогресс - Без помех (Obstruction-free) - Без блокировок (Lock-free) - Без ожидания (Wait-free)
  • 11. Регистры • Разделяемые регистры – базовый объект для общения потоков между собой interface Register<T> { T read(); void write(T val); }
  • 12. Классификация регистров • Безопасные (safe), регулярные (regular), атомарные (atomic) • Один читатель, много читателей (SR, MR) • Один писатель, много писателей (SW, MW) • Булевские значение, множественные значения • Самый примитивный регистр – Safe SRSW Boolean register • Самый сложный регистр – Atomic MRMW M-Valued register
  • 13. Построение регистров • Будем строить более сложные регистры из более простых без ожиданий (wait-free образом). - Safe SRSW Boolean register - Regular SRSW Boolean register - Regular SRSW M-Valued register - Atomic SRSW M-Valued register - Atomic MRSW M-Valued register - Atomic MRMW M-Valued register
  • 14. Атомарный снимок состояния N регистров • Набор SW атомарных регистров (по регистру на поток) • Любой поток может вызвать scan() чтобы получить снимок состояния всех регистров • Методы должны быть атомарными (линеаризуемыми) interface Snapshot<T> { void update(int i, T val); T[] scan(); }
  • 15. Атомарный снимок состояния N регистров, без блокировок (lock free) // каждый регистр хранит версию “version” (T val, Label version) register[N]; void update(int i, T val) { // wait-free register[i] = (val, register[i].version+1); } T[] scan() { // obstruction-free (T, Label)[] old = copyOf(register); // with loop while (true) { (T, Label)[] cur = copyOf(register); if (equal(old, cur)) return cur.val; else old = cur; } }
  • 16. Атомарный снимок состояния N регистров, без ожидания (wait-free) – update // каждый регистр так же хранит копию снимка “snap” (T val, Label version, T[] snap) register[N]; void update(int i, T val) { // wait-free T[] snap = scan(); register[i] = (val, register[i].version+1, snap); }
  • 17. Атомарный снимок состояния N регистров, без ожидания (wait-free) – scan T[] scan() { // wait-free, O(N^2) time (T, Label, T[])[] old = copyOf(register); boolean updated[N]; loop: while (true) { (T, Label, T[])[] cur = copyOf(register); for (int j = 0; j < N; j++) if (cur[j].version != old[j].version) if (updated[j]) return cur[j].snap; else { updated[j] = true; old = cur; continue loop; } return cur.val; } }
  • 18. Консенсус • Согласованность (consistent): все потоки должны вернуть одно и то же значение из метода decide • Обоснованность (valid): возвращенное значение было входным значением какого-то из потоков interface Consensus<T> { T decide(T val); }
  • 19. Консенсусное число • Если с помощью класса объектов C можно реализовать консенсусный протокол без ожидания (wait-free) для N потоков (и не больше), то говорят что у класса C консенсусное число равно N. • ТЕОРЕМА: Атомарные регистры имеют консенсусное число 1. - Т.е. с помощью атомарных регистров даже 2 потока не могут придти к консенсусу без ожидания (докажем от противного) для для 2-х возможных значений при T = {0, 1} - С ожиданием задача решается очевидно (с помощью любого алгоритма взаимного исключения).
  • 20. Определения и леммы для любых классов объектов • Определения и концепции: - Рассматриваем дерево состояния, листья – конечные состояния помеченные 0 или 1 (в зависимости от значения консенсуса). - x-валентное состояние системы (x = 0,1) – консенсус во всех нижестоящих листьях будет x. - Бивалентное состояние – возможен консенсус как 0 так и 1. - Критическое состояние – такое бивалентное состояние, все дети которого одновалентны. • ЛЕММА: Существует начальное бивалентное состояние. • ЛЕММА: Существует критическое состояние.
  • 21. Доказательство для атомарных регистров • Рассмотрим возможные пары операций в критическом состоянии: - Операции над разными регистрами – коммутируют. - Два чтения – коммутируют. - любая операция + Запись – состояние пишущего потока не зависит от порядка операций.
  • 22. Read-Modify-Write регистры • Для функции или класса функций F(args): T -> T - getAndSet (exchange), getAndIncrement, getAndAdd и т.п. - get (read) это тоже [тривиальная] RMW операция для F == id. сlass RMWRegister<T> { private T val; T getAndF(args…) atomic { T old = val; val = F(T, args); return old; } }
  • 23. Нетривиальные RMW регистры • Консенсусное число нетривиального RMW регистра >= 2. - Нужно чтобы была хотя бы одна «подвижная» точка функции F, например F(v0) = v1 != v0. T proposed[2]; RMWRegister rmw; // начальное значение v0 T decide(T val) { int i = myThreadId(); // i = 0,1 – номер потока proposed[i] = val; if (rmw.getAndF() == v0) return proposed[i]; else return proposed[1 – i]; }
  • 24. Common2 RMW регистры • Определения - F1 и F2 коммутируют если F1(F2(x)) == F2(F1(x)) - F1 перезаписывает F2 если F1(F2(x)) == F1(x) - Класс С RMW регистров принадлежит Common2, если любая пара функций либо коммутирует, либо одна из функций перезаписывает другую. • ТЕОРЕМА: Нетривиальный класс Common2 RMW регистров имеет консенсусное число 2. - Третий поток не может отличить глобальное состоянием при изменения порядка выполнения коммутирующих или перезаписывающих операций в критическом состоянии.
  • 25. Универсальные объекты • Объект с консенсусный числом бесконечность называется универсальным объектом. - По определению, с помощью него можно реализовать консенсусный протокол для любого числа потоков. • Пример: compareAndSet aka testAndSet (возвращает boolean), compareAndExchange (возвращает старое значение – RMW) private T x; boolean compareAndSet(T val, T expected) atomic { if (x == expected) { x = val; return true; } else return false; }
  • 26. compareAndSet (CAS) и консенсус // реализация консенсусного протокола через CAS+GET T decide(T val) { if (compareAndSet(val, INITIAL)) return val; else return get(); } // реализация консенсусного протокола через CMPXCHG T decide(T val) { T old = compareAndExchange(val, INITIAL); if (old == INITIAL) return val; else return old; }
  • 27. Универсальность консенсуса • ТЕОРЕМА: Любой последовательный объект можно реализовать без ожидания (wait-free) для N потоков используя консенсусный протокол для N объектов. - Следствие1: С помощью любого класса объектов с консенсусным числом N можно реализовать любой объект с консенсусным числом <= N. - Следствие2: С помощью универсального объекта можно реализовать любой объект.
  • 28. Иерархия объектов Консенсусное Объект число 1 Атомарные регистры, снимок состояния нескольких регистров 2 getAndSet (атомарный обмен), getAndAdd, очередь, стэк m Атомарная запись m регистров из m(m+1)/2 регистров ∞ compareAndSet, LoadLinked/StoreConditional
  • 29. Многопоточные (Thread-Safe) объекты (алгоритмы и структуры данных) на практике • Многопоточный объект включает в себя синхронизацию потоков (блокирующую или не блокирующую), которая позволяет его использовать из нескольких потоков одновременно без дополнительной внешней синхронизации - Специфицируется через последовательное поведение - По умолчанию требуется линеаризуемость операций (более слабые формы согласованности – редко) - Редко удается реализовать все операции без ожидания (wait- free). Часто приходится идти на компромиссные решения. • ВНИМАНИЕ: Пока пишем псевдокод. Доведение его до реального кода будем обсуждать отдельно.
  • 30. Разные подходы к синхронизации потоков при работе с общей структурой данных • Типы синхронизации: - Грубая (Coarse-grained) синхронизация - Тонкая (Fine-grained) синхронизация - Оптимистичная (Optimistic) синхронизация - Ленивая (Lazy) синхронизация - Неблокирующая (Nonblocking) синхронизация • Проще всего для списочных структур данных (с них и начнем), хотя на практике массивы работают существенно быстрей
  • 31. Пример: Множество на основе односвязного списка • ИНВАРИАНТ: node.key < node.next.key class Node { int key; T item; Node next; } // Пустой список состоит их 2-х граничных элементов Node head = new Node(Integer.MIN_VALUE, null); head.next = new Node(Integer.MAX_VALUE, null);
  • 32. Грубая синхронизация • Обеспечиваем взаимное исключение всех операций через общий Mutex lock.
  • 33. Грубая синхронизация: поиск boolean contains(int key) { lock.lock(); try { Node curr = head; while (curr.key < key) { curr = curr.next; } return key == curr.key; } finally { lock.unlock(); } }
  • 34. Грубая синхронизация: добавление boolean add(int key, T item) { lock.lock(); try { Node pred = head, curr = pred.next; while (curr.key < key) { pred = curr; curr = curr.next; } if (key == curr.key) return false; else { Node = new Node(key, item); node.next = curr; pred.next = node; return true; } } finally { lock.unlock(); } }
  • 35. Грубая синхронизация: удаление boolean remove(int key, T item) { lock.lock(); try { Node pred = head, curr = pred.next; while (curr.key < key) { pred = curr; curr = curr.next; } if (key == curr.key) { pred.next = curr.next; return true; else { return false; } } finally { lock.unlock(); } }
  • 36. Тонкая синхронизация • Обеспечиваем синхронизацию через взаимное исключение на каждом элементе. • При любых операциях одновременно удерживаем блокировку текущего и предыдущего элемента (чтобы не утерять инвариант pred.next == curr).
  • 37. Тонкая синхронизация: добавление Node pred = head; pred.lock(); Node curr = pred.next; curr.lock(); try { while (curr.key < key) { pred.unlock(); pred = curr; curr = curr.next; curr.lock(); } if (key == curr.key) return false; else { Node = new Node(key, item); node.next = curr; pred.next = node; return true; } } finally { curr.unlock(); pred.unlock(); }
  • 38. Оптимистичная синхронизация • Ищем элемент без синхронизации (оптимистично предполагая что никто не помешает), но перепроверяем с синхронизацией - Если перепроверка обломалась, то начинаем операцию заново • Имеет смысл только если обход структуры дешев и быстр, а обход с синхронизацией медленный и дорогой
  • 39. Оптимистичная синхронизация: поиск retry: while(true) { Node pred = head, curr = pred.next; while (curr.key < key) { pred = curr; curr = curr.next; } pred.lock(); curr.lock(); try { if (!validate(pred, curr)) continue retry; return curr.key == key; } finally { curr.unlock(); pred.unlock(); } }
  • 40. Оптимистичная синхронизация: валидация boolean validate(Node pred, Node curr) { Node node = head; while (node.key <= pred.key) { if (node == pred) return pred.next == curr; node = node.next; } return false; }
  • 41. Оптимистичная синхронизация: добавление retry: while(true) { Node pred = head, curr = pred.next; while (curr.key < key) { pred = curr; curr = curr.next; } pred.lock(); curr.lock(); try { if (!validate(pred, curr)) continue retry; if (curr.key == key) return false; else { Node = new Node(key, item); node.next = curr; pred.next = node; return true; } } finally { curr.unlock(); pred.unlock(); } }
  • 42. Оптимистичная синхронизация: удаление retry: while(true) { Node pred = head, curr = pred.next; while (curr.key < key) { pred = curr; curr = curr.next; } pred.lock(); curr.lock(); try { if (!validate(pred, curr)) continue retry; if (curr.key == key) { pred.next = curr.next; return true; } else return false; } finally { curr.unlock(); pred.unlock(); } }
  • 43. Ленивая синхронизация • Добавляем в Node поле boolean marked. • Удаление в 2 фазы: - node.marked = true; // Логическое удаление - Физическое удаление из списка • Инвариант: Все непомеченные (неудаленные) элементы всегда в списке • Результат: - Для валидации не надо просматривать список (только проверить что элементы не удалены логически и pred.next == curr). В остальном, код добавление идентичен оптимистичному. - Поиск элемента в списке можно делать без ожидания (wait-free)
  • 44. Ленивая синхронизация: удаление retry: while(true) { Node pred = head, curr = pred.next; while (curr.key < key) { pred = curr; curr = curr.next; } pred.lock(); curr.lock(); try { if (!validate(pred, curr)) continue retry; if (curr.key == key) { curr.marked = true; точка линеаризации pred.next = curr.next; return true; } else return false; } finally { curr.unlock(); pred.unlock(); } }
  • 45. Ленивая синхронизация: валидация boolean validate(Node pred, Node curr) { return !pred.marked && !curr.marked && pred.next == curr; }
  • 46. Ленивая синхронизация: поиск (wait-free!) boolean contains(int key) { Node curr = head; while (curr.key < key) { curr = curr.next; } return key == curr.key && !curr.marked; } точка линеаризации успешного поиска
  • 47. Неблокирующая синхронизация • Простое использование Compare-And-Set (CAS) не помогает – удаления двух соседних элементов будут конфликтовать. • Объединим next и marked в одну переменную {next,marked}, которую будем атомарно менять используя CAS - Каждая операция модификации будет выполнятся одним успешным CAS-ом. - Успешное выполнение CAS-а является точкой линеаризации. • При выполнении операции удаления или добавления будем пытаться произвести физическое удаление - Добавление и удаление будут работать без блокировки (lock-free) - Поиск элемента будет работать без ожидания (wait-free)
  • 48. Неблокирующая синхронизация: добавление retry: while (true) { Node pred, curr; {pred,curr} = find(key); if (curr.key == key) return false; else { Node = new Node(key, item); node.{next,marked} = {curr,false}; линеаризация if (CAS(pred.{next,marked}, {curr,false}, {next,false}) return true; } }
  • 49. Неблокирующая синхронизация: поиск окна {Node,Node} find(int key) { retry: while(true) { Node pred = head, curr = pred.next, succ; while (true) { {succ,boolean cmk} = curr.{next,marked}; if (cmk) { // Если curr логически удален if (!CAS(pred.{next,marked}, {curr,false}, {succ,false})) continue retry; curr = succ; } else { if (curr.key >= key) return {pred,curr}; pred = curr; curr = succ; } } }
  • 50. Неблокирующая синхронизация: удаление retry: while (true) { Node pred, curr; {pred,curr} = find(key); if (curr.key != key) return false; else { Node succ = curr.next; линеаризация if (!CAS(curr.{next,marked}, {next,false}, {next,true}) continue retry; // оптимизация – попытаемся физ. удалить CAS(pred.{next,marked}, {curr,false} {succ,false}); return true; } }
  • 51. Универсальное построение без блокировок с использованием CAS • Вся структура данных представляется как указатель на объект, содержимое которого никогда не меняется. • Любые операции чтения работают без ожидания. • Любые операции модификации создают полную копию структуры, меняют её, из пытаются подменить указать на неё с помощью одного Compare-And-Set (CAS). - В случае ошибки CAS – повтор. • Частный случай этого подхода: вся структура данных влезает в одно машинное слово, например счетчик.
  • 52. Атомарный счетчик int counter; int getAndIncrement(int increment) { retry: while(true) { int old = counter; int updated = old + increment; if (CAS(counter, old, updated)) return old; } }
  • 53. Работа с древовидными структурами без блокировок • Структура представлена в виде дерева • Тогда операции изменения можно реализовать в виде одного CAS, заменяющего указатель на root дерева. - Неизменившуюся часть дерева можно использовать в новой версии дерева, т.е. не нужно копировать всю структуру данных. • Частный случай этого подхода: LIFO стек class Node { T item; Node next; } // Пустой стек это указатель на null Node top = null;
  • 54. Операции с LIFO стеком void push(T item) { retry: while(true) { Node node = new Node(item, top); if (CAS(top, node.next, node)) линеаризация return; } } T pop() { retry: while(true) { Node node = top; if (node == null) throw new EmptyStack(); if (CAS(top, node, node.next)) линеаризация return node.item; } }
  • 55. FIFO очередь без блокировок (lock-free) • Так же односвязный список, но два указателя: head и tail. • Алгоритм придумали Michael & Scott в 1996 году. class Node { T item; Node next; } // Пустой список состоит их одного элемента-заглушки Node head = new Node(null); Node tail = head;
  • 56. Добавление в очередь void enqueue(T item) { Node node = new Node(item); retry: while(true) { Node last = tail, next = last.next; if (next == null) { линеаризация if (!CAS(last.next, null, node)) continue retry; // оптимизация – сами переставляем tail CAS(tail, last, node); return; } // Помогаем другим операциям enqueue с tail CAS(tail, last, next); } }
  • 57. Удаление из очереди T dequeue() { retry: while(true) { Node first = head, last = tail, next = first.next; if (first == last) { if (next == null) throw new EmptyQueue(); // Помогаем операциям enqueue с tail CAS(tail, last, next); } else { линеаризация if (CAS(head, first, next)) return next.item; } } }
  • 58. Работа без GC, проблема ABA • Память освобождается явно через “free” с добавлением в список свободной памяти: - Содержимое ячейки может меняться произвольным образом, пока на неё удерживается указатель • решение – дополнительные перепроверки - CAS может ошибочно отработать из-за проблемы ABA • решение – добавление версии к указателю • Альтернативное решение – свой GC
  • 59. Очереди/стеки на массивах • Структуры на массивах быстрей работают на практике из-за локальности доступа к данным • Очевидные решения не работают - Стек на массиве не работает - Очередь работает только при проходе по памяти один раз (можно развернуть очередь со списками для увеличения быстродействия) • Неочевидные решения - Дек без помех (Obstruction-free Deque) - DCAS/CASn (Обновление нескольких слов одновременно) - Используя дескрипторы операций (универсальная конструкция)
  • 60. Дек без помех • Каждый элемента массива должен содержать элемент и версию, которые мы будем атомарно обновлять CAS-ом • Пустые элементы будут заполнены правыми и левыми нулями (RN и LN) • Указатели на правый и левый край будут храниться «приблизительно» и подбираться перед выполнением операций с помощью оракула (rightOracle и leftOracle) // массив на MAX элементов (0..MAX-1) {T item, int ver} a[MAX]; int left, right; // прибл. указатель на LN и RN
  • 61. Оракул для поиска правого края // Должен находить такое место что: // a[k] == RN && a[k – 1] != RN // Должен корректно работать «без помех» int rightOracle() { int k = right; // только для оптимизации while (a[k] != RN) k++; while (a[k-1] == RN) k--; right = k; // запомнили для оптимизации return k; }
  • 62. Добавление справа void rightPush(T item) { retry: while(true) { int k = rightOracle(); {T item,int ver} prev = a[k-1], cur = a[k]; if (prev.item == RN || cur.item != RN) continue; if (k == MAX-1) throw new FullDeque(); if (CAS(a[k-1], prev, {prev.item,prev.ver+1} && CAS(a[k], cur, {item,cur.ver+1}) return; // успешно закончили }
  • 63. Удаление справа T rightPop() { retry: while(true) { int k = oracleRight(); {T item,int ver} cur = a[k-1], next = a[k]; if (cur.item == RN || next.item != RN) continue; if (cur.item == LN) throw new EmptyDeque(); if (CAS(a[k], next, {RN,next.ver+1} && CAS(a[k-1], cur, {RN,cur.ver+1}) return cur.item; // успешно закончили }
  • 64. Хэш-таблицы • Два основных способа разрешения коллизий - Прямая адресация: каждая ячейка хранит список элементов • Естественный параллелизм, легко делать раздельные блокировки/нарезку блокировок (lock striping) • Применяя алгоритмы работы со списками/множествами можно сделать реализацию без блокировок - Открытая адресация: ищем в других ячейках • На практике быстрей искать в соседних элементах, но требует хэш-функции хорошего качества • Так же возможна реализация без блокировок (занимаем ячейку через CAS), но требует специальной техники удаления • Изменение размера хэш-таблицы (rehash)