Параллельное программирование.pptx
- Количество слайдов: 128
Параллельное программирование Роман Елизаров, 2010 elizarov@devexperts. com
Взаимное исключение • Взаимное исключение (Mutual Exclusion) • Отсутствие блокировки (Freedom from Deadlock) • Отсутствие голодания (Freedom from Starvation) run() { int i = get. Current. Thread. ID(); while (true) { non. Critical. Section(); lock(i); critical. Section(); 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() { Doorway. Section(); // wait-free code Waiting. Section(); }
Взаимное исключение, алгоритм Лампорта (алгоритм булочника – вариант 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
Классификация регистров • Безопасные (safe), регулярные (regular), атомарные (atomic) • Один читатель, много читателей (SR, MR) • Один писатель, много писателей (SW, MW) • Булевские значение, множественные значения • Самый примитивный регистр – Safe SRSW Boolean register • Самый сложный регистр – Atomic MRMW M-Valued register
Построение регистров • Будем строить более сложные регистры из более простых без ожиданий (wait-free образом). - Safe SRSW Boolean register - Regular SRSW M-Valued register - Atomic MRSW M-Valued register - Atomic MRMW M-Valued register
Атомарный снимок состояния N регистров • Набор SW атомарных регистров (по регистру на поток) • Любой поток может вызвать scan() чтобы получить снимок состояния всех регистров • Методы должны быть атомарными (линеаризуемыми) interface Snapshot
Атомарный снимок состояния 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 = copy. Of(register); // with loop while (true) { (T, Label)[] cur = copy. Of(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 = copy. Of(register); boolean updated[N]; loop: while (true) { (T, Label, T[])[] cur = copy. Of(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
Консенсусное число • Если с помощью класса объектов 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 - get. And. Set (exchange), get. And. Increment, get. And. Add и т. п. - get (read) это тоже [тривиальная] RMW операция для F == id. сlass RMWRegister
Нетривиальные RMW регистры • Консенсусное число нетривиального RMW регистра >= 2. - Нужно чтобы была хотя бы одна «подвижная» точка функции F, например F(v 0) = v 1 != v 0. T proposed[2]; RMWRegister rmw; // начальное значение v 0 T decide(T val) { int i = my. Thread. Id(); // i = 0, 1 – номер потока proposed[i] = val; if (rmw. get. And. F() == v 0) return proposed[i]; else return proposed[1 – i]; }
Common 2 RMW регистры • Определения - F 1 и F 2 коммутируют если F 1(F 2(x)) == F 2(F 1(x)) - F 1 перезаписывает F 2 если F 1(F 2(x)) == F 1(x) - Класс С RMW регистров принадлежит Common 2, если любая пара функций либо коммутирует, либо одна из функций перезаписывает другую. • ТЕОРЕМА: Нетривиальный класс Common 2 RMW регистров имеет консенсусное число 2. - Третий поток не может отличить глобальное состоянием при изменения порядка выполнения коммутирующих или перезаписывающих операций в критическом состоянии.
Универсальные объекты • Объект с консенсусный числом бесконечность называется универсальным объектом. - По определению, с помощью него можно реализовать консенсусный протокол для любого числа потоков. • Пример: compare. And. Set aka test. And. Set (возвращает boolean), compare. And. Exchange (возвращает старое значение – RMW) private T x; boolean compare. And. Set(T val, T expected) atomic { if (x == expected) { x = val; return true; } else return false; }
compare. And. Set (CAS) и консенсус // реализация консенсусного протокола через CAS+GET T decide(T val) { if (compare. And. Set(val, INITIAL)) return val; else return get(); } // реализация консенсусного протокола через CMPXCHG T decide(T val) { T old = compare. And. Exchange(val, INITIAL); if (old == INITIAL) return val; else return old; }
Универсальность консенсуса • ТЕОРЕМА: Любой последовательный объект можно реализовать без ожидания (wait-free) для N потоков используя консенсусный протокол для N объектов. - Следствие 1: С помощью любого класса объектов с консенсусным числом N можно реализовать любой объект с консенсусным числом <= N. - Следствие 2: С помощью универсального объекта можно реализовать любой объект.
Иерархия объектов Консенсусное число Объект 1 Атомарные регистры, снимок состояния нескольких регистров 2 get. And. Set (атомарный обмен), get. And. Add, очередь, стэк m Атомарная запись m регистров из m(m+1)/2 регистров ∞ compare. And. Set, Load. Linked/Store. Conditional
Многопоточные (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(); 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(); 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(); 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 get. And. Increment(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 Empty. Stack(); if (CAS(top, 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 Empty. Queue(); // Помогаем операциям 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) • Указатели на правый и левый край будут храниться «приблизительно» и подбираться перед выполнением операций с помощью оракула (right. Oracle и left. Oracle) // массив на MAX элементов (0. . MAX-1) {T item, int ver} a[MAX]; int left, right; // прибл. указатель на LN и RN
Оракул для поиска правого края // Должен находить такое место что: // a[k] == RN && a[k – 1] != RN // Должен корректно работать «без помех» int right. Oracle() { int k = right; // только для оптимизации while (a[k] != RN) k++; while (a[k-1] == RN) k--; right = k; // запомнили для оптимизации return k; }
Добавление справа void right. Push(T item) { retry: while(true) { int k = right. Oracle(); {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 Full. Deque(); if (CAS(a[k-1], prev, {prev. item, prev. ver+1} && CAS(a[k], cur, {item, cur. ver+1}) return; // успешно закончили }
Удаление справа T right. Pop() { retry: while(true) { int k = oracle. Right(); {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 Empty. Deque(); 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)
Что нужно сделать, чтобы корректные программы корректно работали? int flag, value; // общие переменные, обе 0 в начале void init() { value = 2; flag = 1; } int take() { while (flag == 0); // ждем return value; } // Какие возможны значения результата take() // при параллельной работе с init() ?
Проблемы и обходные пути • Оптимизации в компиляторах - изменение порядка операций, устранений общих подвыражений, использование регистров и т. п. - Обходной путь: отключить оптимизации, использовать «непрозрачные» для компилятора внешние вызовы • Оптимизации в процессорах - буфера для записи, спекулятивное чтение, кэши, несимметричная память и т. п. - Обходной путь: специальные команды «синхронизации» (membar, fence) которые заставляют процессор сделать видимость последовательного исполнения в ущерб производительности
Модель [согласованности] памяти • Контракт между средой исполнения (компиляторы + процессор) • Идеал для программиста – последовательная согласованность - но слишком «дорого» (нельзя делать никаких оптимизаций) • Нужно определять более слабую модель (работа над моделью для C++ еще продолжается рамках работы над C++0 x)
Java Memory Model • Зачем Java модель памяти? - Трюки используемые C/C++ программистами не работают в «чистом» Java коде без нативных методов. - Java работает на широком классе платформ с разнообразной архитектурой. - Два этапа компиляции (Java source -> Byte code и Byte code -> Native Code) усложняют анализ того, что может случиться с программой и у программиста нет прямого контроля над конечным нативным кодом • Нет возможности написать #ifdef SOME_ARCH …. и т. п.
Основы JMM • Межпоточные действия (vs внутрипоточные действия) - Обычные: чтение и запись разделяемых переменных - Операции синхронизации • Чтение и запись переменных volatile • Блокировка и разблокировка (вход и выход в synchronized) • Запуск/останов потоков и прочее • Отношение синхронизации (synchronizes-with) и отношение произошло-до (happens-before) • Понятие конфликтующего доступа (conflicting access) и гонки за данными (data race, race condition) - Понятие «корректно синхронизированной программы»
Гарантии JMM • Выполнение корректно синхронизированной программы будет выглядеть последовательно согласовано. • Гонки за данными не могут нарушить базовые гарантии безопасности платформы: - Система типов (instanceof и т. п. ), длинны массивов - Все типы кроме long и double пишутся и читаются атомарно даже в отсутствии синхронизации - Все поля гарантировано инициализированы нулями (нельзя увидеть там «мусор» ) - Дополнительные гарантии для неизменяемых объектов (при использовании final полей)
Рабочий вариант #1 volatile int flag; // всего один volatile // … решает проблемы видимости и упорядоченности int value; void init() { value = 2; flag = 1; } int take() { while (flag == 0); // ждем… кушаем CPU return value; }
Рабочий вариант #2 public class Value { int flag, value; void synchronized init() { value = 2; // здесь порядок уже не важен flag = 1; notify. All(); // разбудить ожидания } int synchronized take() throws Interrupted. Exception { while (flag == 0) wait(); return value; } }
CASN: Задача // Хотим создать класс (на Java) public class Data. Reference
CASN: Логика работы (псевдокод) • Хотим чтобы работало корректно (линеаризуемо) и: - Lock-free (без блокировок) - Disjoint-Access Parallel (непересекающиеся доступы ||-ны) boolean CASN(CASEntry. . . entries) atomic { for (CASEntry entry: entries) if (entry. a. value != entry. expect) return false; for (CASEntry entry: entries) entry. a. value = entry. update; return true; }
CASN: Что есть в Java чтобы начать? import java. util. concurrent. atomic. Atomic. Reference. Field. Updater; // пишем в классе Data. Reference private static final Atomic. Reference. Field. Updater
CASN: Теперь напишем get. And. CAS // псевдокод Object get. And. CAS(Data. Reference a, Object expect, Object update) atomic { Object curval = a. value; if (a. value == expect) a. value = update; return curval; } // реализация в классе Data. Reference Object get. And. CAS(Object expect, Object update) { do { Object curval = value; if (curval != expect) return curval; } while (!CAS(expect, update)); return expect; }
CASN: Промежуточная операция DCSS (Double-Compare Single-Set) // псевдокод Object DCSS(Data. Reference a 1, Object expect 1, Data. Reference a 2, Object expect 2, Object update 2) atomic { Object curval 2 = a 2. value; if (a 1. value == expect 1 && a 2. value == expect 2) a 2. value = update 2; return curval 2; } // Реализуем ограниченную (Restricted) версию – RDCSS // a 1 – только из специальной «контрольной» секции // a 2 – произвольные данные
СASN: RDCSSDescriptor class RDCSSDescriptor { private final Data. Reference a 1; private final Object expect 1; private final Data. Reference a 2; private final Object expect 2; private final Object update 2; // и простой конструктор для всех 5 полей … } // будем вызывать операцию RDCSS так: new RDCSSDescriptor( a 1, expect 1, a 2, expect 2, update 2). invoke(); // сейчас напишем метод invoke
CASN: Реализация RDCSS // в классе RDCSSDescriptor Object invoke() { Object r; do { r = a 2. get. And. CAS(expect 2, this); if (r instanceof RDCSSDescriptor) ((RDCSSDescriptor)r). complete(); } while (r instanceof RDCSSDescriptor); if (r == expect 2) complete(); return r; } void complete() { if (a 1. value == expect 1) a 2. CAS(this, update 2); else a 2. CAS(this, expect 2); }
CASN: Как прочитать RDCSS значение? // в классе Data. Reference public T get() { while (true) { Object curval = value; if (curval instanceof RDCSSDescriptor) { ((RDCSSDescriptor)curval). complete(); continue; // retry } return (T)curval; // not a descriptor } } // Этого не нужно для «контрольной» секции RDCSS (a 1) // там никогда не может оказаться RDCSSDescriptor
CASN: CASNDescriptor class CASNDescriptor { private final Data. Reference status = new Data. Reference(Status. UNDECIDED); private final CASEntry[] entries; // и простой конструктор для entries } enum Status { UNDECIDED, SUCCEEDED, FAILED }
CASN: 1 -ая фаза // в классе CASNDescriptor boolean complete() { if (status. value == Status. UNDECIDED) { Status new. Status = Status. SUCCEEDED; for (int i = 0; i < entries. length; ) { CASEntry entry = entries[i]; Object val = new RDCSSDescriptor( this. status, Status. UNDECIDED, entry. a, entry. expect, this). invoke(); if (val instanceof CASNDescriptor) { if (val != this) { ((CASNDescriptor)val). complete(); continue; // retry this entry } } else if (val != entry. expect) { new. Status = Status. FAILED; break; } i++; // go to next entry } // end for this. status. CAS(Status. UNDECIDED, new. Status); }
CASN: 2 -ая фаза // в классе CASNDescriptor // … продолжаем метод complete boolean succeeded = status. value == Status. SUCCEEDED; for (CASEntry entry : entries) entry. a. CAS(this, succeeded ? entry. update : entry. expect); return succeeded; } // конец метода complete
CASN: Как прочитать значение? // в классе Data. Reference обновим метод public T get() { while (true) { Object curval = value; if (curval instanceof RDCSSDescriptor) { ((RDCSSDescriptor)curval). complete(); continue; // retry } if (curval instanceof CASNDescriptor) { ((CASNDescriptor)curval). complete(); continue; // retry } return (T)curval; // not a descriptor } }
CASN: Как гарантировать прогресс? • Надо гарантировать одинаковый порядок обработки Data. Reference каждым CASN… Их надо как-то упорядочить // конструктор в классе СASNDescriptor CASNDescriptor(CASEntry[] entries) { this. entries = entries; Arrays. sort(this. entries); }
Анализ конфликтов • Гонка (конфликт) данных (data race): два [несинхронизированных] доступа к одной ячейке данных, один из которых запись. • Матрица конфликтов (X – конфликт): R R W W X X X
Пример: стек на массиве (однопоточный) public class Array. Stack
Анализ конфликтов стека size pop X size push X X X pop X X X
Пример: стек с грубой синхронизацией public class Array. Stack
Пример: стек с грубой синхронизацией • Теперь нет гонок по данным. Почему? public synchronized int size() { return top; } // в Java полностью эквивалентно: public int size() { synchronized (this) { // bytecode -- monitorenter return top; } // bytecode -- monitorexit }
Пример: стек с грубой синхронизацией 2 import java. util. concurrent. locks. *; // В классе Array. Stack в дополнение к данным: private final Lock lock = new Reentrant. Lock(); public int size() { try { lock(); return top; } finally { lock. unlock(); } }
Пример: стек с грубой синхронизацией 2 // В классе Array. Stack продолжаем: public void push(T item) { lock(); try { data[top++] = item; } finally { lock. unlock(); } } // аналогично pop
Матрица совместимости блокировок с грубой синхронизацией size push pop size X лишнее X X push X X X pop X X X
Read-write locks (блокировка чтения-записи) • Это специальные локи, со следующей матрицей совместимости (X – несовместимые блокировки) - Read aka Shared Lock - Write aka Exclusive Lock • Они идеально подходят для грубой защиты структур данных в которых есть конфликты (гонки) по данным. R R W W X X X
Пример: стек с грубой синхронизацией 3 // В классе Array. Stack в дополнение к данным: private final Read. Write. Lock lock = new Reentrant. Read. Write. Lock(); public int size() { try { lock. read. Lock(). lock(); return top; } finally { lock. read. Lock(). unlock(); } }
Пример: стек с грубой синхронизацией 3 // В классе Array. Stack продолжаем: Public void push(T item) { lock. write. Lock(). lock(); try { data[top++] = item; } finally { lock. write. Lock(). unlock(); } } // аналогично pop
Матрица совместимости с блокировками чтения-записи с грубой синхронизацией size pop X size push X X X pop X X X Полностью повторяет матрицу конфликтов. Лучше сделать нельзя.
Cтек на массивах используя CASN // в классе Array. Stack: private final Data. Reference
Cтек на массивах используя CASN // в классе Array. Stack: public void push(T newitem) { Integer curtop; do { curtop = top. get(); } while (!Data. Reference. CASN( new CASEntry
Обзор методов создания многопоточных объектов (уже рассмотренных) Или как сделать линеаризуемый многопоточный объект? • Блокировка (aka синхронизация) - Грубая, тонкая, оптимистичная, ленивая - Read-Write • Без блокировки - Универсальная конструкция (сopy-on-write + CAS, частичное копирование + CAS) - CASN - Алгоритмы специфичные для структуры данных (пример: cписокмножество, очередь без блокировки Майкла-Скота, дэк без помех и т. п. )
Недостатки блокировки • В системе нет прогресса, пока объект заблокирован - Инверсия приоритетов • Требуются дополнительные переключения контекста чтобы дать закончить работу блокирующему потоку - Может съедать существенную долю CPU времени системы • Минимальный параллелизм работы - Но чем меньше блокировки, тем больше параллелизм • Взаимные блокировки (deadlocks)
Общая проблема: построение составных объектов (composability) // Небезопасный объект! public class Employees { Set working = new Concurrent. Set(); // thread-safe Set vacating = new Concurrent. Set(); // thread-safe public boolean contains(Employee e) { return working. contains(e) || vacating. contains(e); } public void start. Vacation(Employee e) { working. remove(e); vacating. add(e); } }
Решение: Software Transactional Memory (STM) • Классические транзакции: - Atomicity - Consistency - Isolation - Durability
Поддержка на уровне языка // если бы можно было бы написать так… public boolean contains(Employee e) { atomic { return working. contains(e) || vacating. contains(e); } } public void start. Vacation(Employee e) { atomic { working. remove(e); vacating. add(e); } }
Без поддержки на уровне языка // придется написать так… public boolean contains(final Employee e) { return Transaction. atomic( new Atomic. Block
Transaction public class Transaction { public static
Реализация транзакций public class TVar
Реализация транзакций с блокировками • Двухфазная блокировка (2 PL = 2 Phase Locking) - Все конфликтующие операции защищаются локами, исключающими конфликты. - В начале транзакции (первая фаза) локи накапливаются. - В конце транзакции (вторая фаза) локи освобождаются • Основная теорема: Любое допустимое исполнение такой системы будет линеаризуемо.
Реализация транзакций с блокировками // В классе TVar добавляем… private final Read. Write. Lock lock = new Reentrant. Read. Write. Lock(); public T get() { lock. read. Lock(). lock(); Transaction. current. Transaction(). add. Lock(lock. read. Lock()); return value; } public void set(T value) { lock. write. Lock(). lock(); Transaction. current. Transaction(). add. Lock(lock. write. Lock()); this. value = value; }
Реализация транзакций с блокировками // В классе Transaction добавляем… private static final Thread. Local
сommit с блокировками // В классе Transaction добавляем… public boolean commit() { for (Lock lock : locks) lock. unlock(); return true; } // а вот rollback сделать не так очевидно… // В классе TVar добавляем private static final Object UNDEFINED = new Object(); private Object old. Value = UNDEFINED;
rollback с блокировками 1 // В классе TVar добавляем public void set(T value) { if (old. Value == UNDEFINED) { lock. write. Lock(). lock(); this. old. Value = this. value; Transaction. current. Transaction(). add. Write(this); } this. value = value; } void rollback() { value = (T)old. Value; old. Value = UNDEFINED; lock. write. Lock(). unlock(); }
rollback с блокировками 2 // В классе Transaction добавляем… private final Set
Реализация транзакций с блокировками • Проблемы – прогресс - Взаимные блокировки (deadlocks) • Нужно написать - Предотвращение взаимных блокировок (deadlock avoidance) - Определение взаимных блокировок (deadlock detection) • Например, по истечению времени ожидания лока - Устранение взаимных блокировок (deadlock resolution) • Откатывая и начиная снова одну из заблокированных транзакций
Реализация транзакций без блокировок public class Transaction { private static final int ACTIVE = 0; private static final int COMMITTED = 1; private static final int ABORTED = -1; private final Atomic. Integer state = new Atomic. Integer(ACTIVE); public boolean is. Committed() { return state. get() == COMMITTED; } public boolean commit() { return state. compare. And. Set(ACTIVE, COMMITTED); } public void rollback() { state. compare. And. Set(ACTIVE, ABORTED); }
Хранение значений без блокировок // Реализуем вспомогательный объект class Var. Holder
«Открытие» переменной без блокировок public class TVar
Доступ к переменной без блокировок // В классе TVar
Реализация транзакций без блокировок • Это реализация без помех (obstruction-free) - Разные потоки могут бесконечно долго другу мешать закончить транзакцию без прогресса - Но если активен только один поток, то прогресс гарантирован • Сравните с версией с блокировками - На практике, нужно управлять конфликтами (contention manager), чтобы отслеживать конфликты и увеличивать прогресс • Проблема - Даже читающие транзакции мешают другу • Предыдущий алгоритм с блокировками был здесь лучше
Параллельное чтение без блокировок // В классе TVar не будем открывать при чтении public T get() { return holder. get(). current(); } • Проблема: Значение может поменяться в процессе работы транзакции (non-repeatable read) - А значит, нет линеаризуемости транзакций • Решения: - Перепроверка корректности во время завершения транзакции - Многоверсионный контроль корректности (MVCC – Multiversion Concurrency Control)
Перепроверка корректности во время завершения транзакции - read // В классе Transaction запомним прочитанные значения private final Map
Перепроверка корректности во время завершения транзакции - commit // В классе Transaction будем открывать прочитанные // переменные перед самым завершением транзакции public boolean commit() { for (Map. Entry
Перепроверка корректности во время завершения транзакции - get // В классе TVar будем использовать read public T get() { return Transaction. current(). read(this); } • Конфликт по чтению остался только на время commit - Можно использовать глобальную блокировку на короткое время операции commit • Алгоритм потеряет свойство «без блокировок» • Алгоритм перестанет быть DAP (disjoint access parallel) - Можно не открывать прочитанные переменные в commit, а написать алгоритм завершения без ожидания, аналогичный DCSS (double-compare single-swap), чтобы commit происходил только если все прочитанные и не измененные переменные имеют свои старые значения
Перепроверка корректности во время завершения транзакции - корректность • Есть ли у этого алгоритма проблема ABA? - Нет. Мы считает транзакцию выполненной в момент вызова commit, поэтому нам важно, чтобы прочитанные значения были ожидаемыми к этому моменты (не важно что они менялись) • Линеаризуемо ли исполнение этого алгоритма? - Нет. Этот алгоритм не гарантирует атомарность снимка состояния памяти при чтения. Значения одних переменных могут прочитаться на один момент времени, других – на другой. - Решение: Контроль версий значений • Придется глобально нумеровать все транзакции • Придется потерять свойство DAP из-за этого
Версионность значений – нумерация транзакций // В классе Transaction будем присваивать номер // Следующий номер – из-за него потеряем DAP private static final Atomic. Long SEQUENCE = new Atomic. Long(); // Наш номер для чтения данных final long sequence = SEQUENCE. get. And. Increment(); // Нам номер завершения (он же статус, -1 = ABORTED) private final Atomic. Long state = new Atomic. Long(ACTIVE); // Будем хранить номера прочитанных версий private final Map
Версионность значений – версия изменений // В классе Var. Holder будем хранить final Transaction owner; final long version; // версия к [старому] value final Object value; Object new. Value; // updated by owner // И инициализировать вот так при изменении Var. Holder(Transaction owner, long version, Object value) { this. owner = owner; this. version = version; this. value = value; this. new. Value = value; }
Версионность значений – чтение // В классе Transaction
Версионность значений – проверка // В классе Transaction public boolean commit() { for (Map. Entry


