Скачать презентацию Параллельное программирование Роман Елизаров 2010 elizarov devexperts com Скачать презентацию Параллельное программирование Роман Елизаров 2010 elizarov devexperts com

Параллельное программирование.pptx

  • Количество слайдов: 128

Параллельное программирование Роман Елизаров, 2010 elizarov@devexperts. com Параллельное программирование Роман Елизаров, 2010 elizarov@devexperts. com

Взаимное исключение • Взаимное исключение (Mutual Exclusion) • Отсутствие блокировки (Freedom from Deadlock) • Взаимное исключение • Взаимное исключение (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 Взаимное исключение, попытка 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 Взаимное исключение, попытка 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; Взаимное исключение, алгоритм Петерсона 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) { Взаимное исключение, алгоритм Петерсона для 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 Взаимное исключение, алгоритм Лампорта (алгоритм булочника – вариант 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 Взаимное исключение, алгоритм Лампорта (алгоритм булочника – вариант 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) - Последовательная согласованность Разделяемые объекты • Корректность реализации объекта - Тихая согласованность (Quiescent consistency) - Последовательная согласованность (Sequential consistency) - Линеаризуемость (Linearizability) • Прогресс - Без помех (Obstruction-free) - Без блокировок (Lock-free) - Без ожидания (Wait-free)

Регистры • Разделяемые регистры – базовый объект для общения потоков между собой interface Register<T> Регистры • Разделяемые регистры – базовый объект для общения потоков между собой interface Register { T read(); void write(T val); }

Классификация регистров • Безопасные (safe), регулярные (regular), атомарные (atomic) • Один читатель, много читателей Классификация регистров • Безопасные (safe), регулярные (regular), атомарные (atomic) • Один читатель, много читателей (SR, MR) • Один писатель, много писателей (SW, MW) • Булевские значение, множественные значения • Самый примитивный регистр – Safe SRSW Boolean register • Самый сложный регистр – Atomic MRMW M-Valued register

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

Атомарный снимок состояния N регистров • Набор SW атомарных регистров (по регистру на поток) Атомарный снимок состояния N регистров • Набор SW атомарных регистров (по регистру на поток) • Любой поток может вызвать scan() чтобы получить снимок состояния всех регистров • Методы должны быть атомарными (линеаризуемыми) interface Snapshot { void update(int i, T val); T[] scan(); }

Атомарный снимок состояния N регистров, без блокировок (lock free) // каждый регистр хранит версию Атомарный снимок состояния 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 // каждый регистр так Атомарный снимок состояния 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() { // Атомарный снимок состояния 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): все потоки должны вернуть одно и то же значение из Консенсус • Согласованность (consistent): все потоки должны вернуть одно и то же значение из метода decide • Обоснованность (valid): возвращенное значение было входным значением какого-то из потоков interface Consensus { T decide(T val); }

Консенсусное число • Если с помощью класса объектов C можно реализовать консенсусный протокол без Консенсусное число • Если с помощью класса объектов 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. Read-Modify-Write регистры • Для функции или класса функций F(args): T -> T - get. And. Set (exchange), get. And. Increment, get. And. Add и т. п. - get (read) это тоже [тривиальная] RMW операция для F == id. сlass RMWRegister { private T val; T get. And. F(args…) atomic { T old = val; val = F(T, args); return old; } }

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

Иерархия объектов Консенсусное число Объект 1 Атомарные регистры, снимок состояния нескольких регистров 2 get. Иерархия объектов Консенсусное число Объект 1 Атомарные регистры, снимок состояния нескольких регистров 2 get. And. Set (атомарный обмен), get. And. Add, очередь, стэк m Атомарная запись m регистров из m(m+1)/2 регистров ∞ compare. And. Set, Load. Linked/Store. Conditional

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

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

Пример: Множество на основе односвязного списка • ИНВАРИАНТ: node. key < node. next. key Пример: Множество на основе односвязного списка • ИНВАРИАНТ: 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. Грубая синхронизация • Обеспечиваем взаимное исключение всех операций через общий Mutex lock.

Грубая синхронизация: поиск boolean contains(int key) { lock(); try { Node curr = head; Грубая синхронизация: поиск 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 Грубая синхронизация: добавление 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 Грубая синхронизация: удаление 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; Тонкая синхронизация: добавление 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; Оптимистичная синхронизация: поиск 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 Оптимистичная синхронизация: валидация 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; Оптимистичная синхронизация: добавление 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; Оптимистичная синхронизация: удаление 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 поле boolean marked. • Удаление в 2 фазы: - node. marked = true; // Логическое удаление - Физическое удаление из списка • Инвариант: Все непомеченные (неудаленные) элементы всегда в списке • Результат: - Для валидации не надо просматривать список (только проверить что элементы не удалены логически и pred. next == curr). В остальном, код добавление идентичен оптимистичному. - Поиск элемента в списке можно делать без ожидания (wait-free)

Ленивая синхронизация: удаление retry: while(true) { Node pred = head, curr = pred. next; Ленивая синхронизация: удаление 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. Ленивая синхронизация: валидация 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. Ленивая синхронизация: поиск (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) не помогает – удаления двух соседних элементов Неблокирующая синхронизация • Простое использование 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); Неблокирующая синхронизация: добавление 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 Неблокирующая синхронизация: поиск окна {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); Неблокирующая синхронизация: удаление 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 • Вся структура данных представляется как указатель Универсальное построение без блокировок с использованием CAS • Вся структура данных представляется как указатель на объект, содержимое которого никогда не меняется. • Любые операции чтения работают без ожидания. • Любые операции модификации создают полную копию структуры, меняют её, из пытаются подменить указать на неё с помощью одного Compare-And-Set (CAS). - В случае ошибки CAS – повтор. • Частный случай этого подхода: вся структура данных влезает в одно машинное слово, например счетчик.

Атомарный счетчик int counter; int get. And. Increment(int increment) { retry: while(true) { int Атомарный счетчик 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 = Операции с 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 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) Добавление в очередь 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 Удаление из очереди 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” с добавлением в Работа без 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] == Оракул для поиска правого края // Должен находить такое место что: // 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. Добавление справа 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 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; // общие переменные, Что нужно сделать, чтобы корректные программы корректно работали? 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 Memory Model • Зачем Java модель памяти? - Трюки используемые C/C++ программистами не работают в «чистом» Java коде без нативных методов. - Java работает на широком классе платформ с разнообразной архитектурой. - Два этапа компиляции (Java source -> Byte code и Byte code -> Native Code) усложняют анализ того, что может случиться с программой и у программиста нет прямого контроля над конечным нативным кодом • Нет возможности написать #ifdef SOME_ARCH …. и т. п.

Основы JMM • Межпоточные действия (vs внутрипоточные действия) - Обычные: чтение и запись разделяемых Основы JMM • Межпоточные действия (vs внутрипоточные действия) - Обычные: чтение и запись разделяемых переменных - Операции синхронизации • Чтение и запись переменных volatile • Блокировка и разблокировка (вход и выход в synchronized) • Запуск/останов потоков и прочее • Отношение синхронизации (synchronizes-with) и отношение произошло-до (happens-before) • Понятие конфликтующего доступа (conflicting access) и гонки за данными (data race, race condition) - Понятие «корректно синхронизированной программы»

Гарантии JMM • Выполнение корректно синхронизированной программы будет выглядеть последовательно согласовано. • Гонки за Гарантии JMM • Выполнение корректно синхронизированной программы будет выглядеть последовательно согласовано. • Гонки за данными не могут нарушить базовые гарантии безопасности платформы: - Система типов (instanceof и т. п. ), длинны массивов - Все типы кроме long и double пишутся и читаются атомарно даже в отсутствии синхронизации - Все поля гарантировано инициализированы нулями (нельзя увидеть там «мусор» ) - Дополнительные гарантии для неизменяемых объектов (при использовании final полей)

Рабочий вариант #1 volatile int flag; // всего один volatile // … решает проблемы Рабочий вариант #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() { Рабочий вариант #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<T> { // CASN: Задача // Хотим создать класс (на Java) public class Data. Reference { // внутреннее значение volatile Object value; // public API методы public T get(); public static boolean CASN(CASEntry. . . entries); } // где public class CASEntry { final Data. Reference a; // что поменять final T expect; // что ожидаем final T update; // на что заменить // и простой конструктор для всех 3 -х полей … }

CASN: Логика работы (псевдокод) • Хотим чтобы работало корректно (линеаризуемо) и: - Lock-free (без 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. CASN: Что есть в Java чтобы начать? import java. util. concurrent. atomic. Atomic. Reference. Field. Updater; // пишем в классе Data. Reference private static final Atomic. Reference. Field. Updater VALUE_UPDATER = Atomic. Reference. Field. Updater. new. Updater( Data. Reference. class, Object. class, "value"); boolean CAS(Object expect, Object update) { return VALUE_UPDATER. compare. And. Set( this, expect, update); }

CASN: Теперь напишем get. And. CAS // псевдокод Object get. And. CAS(Data. Reference a, 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 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 С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 { 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() { 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. 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 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 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() 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… CASN: Как гарантировать прогресс? • Надо гарантировать одинаковый порядок обработки Data. Reference каждым CASN… Их надо как-то упорядочить // конструктор в классе СASNDescriptor CASNDescriptor(CASEntry[] entries) { this. entries = entries; Arrays. sort(this. entries); }

Анализ конфликтов • Гонка (конфликт) данных (data race): два [несинхронизированных] доступа к одной ячейке Анализ конфликтов • Гонка (конфликт) данных (data race): два [несинхронизированных] доступа к одной ячейке данных, один из которых запись. • Матрица конфликтов (X – конфликт): R R W W X X X

Пример: стек на массиве (однопоточный) public class Array. Stack<T> { int top; T[] data; Пример: стек на массиве (однопоточный) public class Array. Stack { int top; T[] data; // конструктор выделяет массив data public int size() { return top; } public void push(T item) { data[top++] = item; } public T pop() { return data[--top]; } }

Анализ конфликтов стека size pop X size push X X X pop X X Анализ конфликтов стека size pop X size push X X X pop X X X

Пример: стек с грубой синхронизацией public class Array. Stack<T> { private int top; private Пример: стек с грубой синхронизацией public class Array. Stack { private int top; private T[] data; // конструктор выделяет массив data public synchronized int size() { return top; } public synchronized void push(T item) { data[top++] = item; } public synchronized T pop() { return data[--top]; } }

Пример: стек с грубой синхронизацией • Теперь нет гонок по данным. Почему? public synchronized Пример: стек с грубой синхронизацией • Теперь нет гонок по данным. Почему? public synchronized int size() { return top; } // в Java полностью эквивалентно: public int size() { synchronized (this) { // bytecode -- monitorenter return top; } // bytecode -- monitorexit }

Пример: стек с грубой синхронизацией 2 import java. util. concurrent. locks. *; // В Пример: стек с грубой синхронизацией 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 Пример: стек с грубой синхронизацией 2 // В классе Array. Stack продолжаем: public void push(T item) { lock(); try { data[top++] = item; } finally { lock. unlock(); } } // аналогично pop

Матрица совместимости блокировок с грубой синхронизацией size push pop size X лишнее X X Матрица совместимости блокировок с грубой синхронизацией size push pop size X лишнее X X push X X X pop X X X

Read-write locks (блокировка чтения-записи) • Это специальные локи, со следующей матрицей совместимости (X – Read-write locks (блокировка чтения-записи) • Это специальные локи, со следующей матрицей совместимости (X – несовместимые блокировки) - Read aka Shared Lock - Write aka Exclusive Lock • Они идеально подходят для грубой защиты структур данных в которых есть конфликты (гонки) по данным. R R W W X X X

Пример: стек с грубой синхронизацией 3 // В классе Array. Stack в дополнение к Пример: стек с грубой синхронизацией 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 Пример: стек с грубой синхронизацией 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 Матрица совместимости с блокировками чтения-записи с грубой синхронизацией size pop X size push X X X pop X X X Полностью повторяет матрицу конфликтов. Лучше сделать нельзя.

Cтек на массивах используя CASN // в классе Array. Stack: private final Data. Reference<Integer> Cтек на массивах используя CASN // в классе Array. Stack: private final Data. Reference top = new Data. Reference(0); private final Data. Reference[] data; // конструктор создает и инициализирует data public int size() { return top. get(); }

Cтек на массивах используя CASN // в классе Array. Stack: public void push(T newitem) Cтек на массивах используя CASN // в классе Array. Stack: public void push(T newitem) { Integer curtop; do { curtop = top. get(); } while (!Data. Reference. CASN( new CASEntry(top, curtop + 1), new CASEntry(data[curtop], null, newitem))); } // похожим образом pop // есть ли здесь проблема ABA?

Обзор методов создания многопоточных объектов (уже рассмотренных) Или как сделать линеаризуемый многопоточный объект? • Обзор методов создания многопоточных объектов (уже рассмотренных) Или как сделать линеаризуемый многопоточный объект? • Блокировка (aka синхронизация) - Грубая, тонкая, оптимистичная, ленивая - Read-Write • Без блокировки - Универсальная конструкция (сopy-on-write + CAS, частичное копирование + CAS) - CASN - Алгоритмы специфичные для структуры данных (пример: cписокмножество, очередь без блокировки Майкла-Скота, дэк без помех и т. п. )

Недостатки блокировки • В системе нет прогресса, пока объект заблокирован - Инверсия приоритетов • Недостатки блокировки • В системе нет прогресса, пока объект заблокирован - Инверсия приоритетов • Требуются дополнительные переключения контекста чтобы дать закончить работу блокирующему потоку - Может съедать существенную долю CPU времени системы • Минимальный параллелизм работы - Но чем меньше блокировки, тем больше параллелизм • Взаимные блокировки (deadlocks)

Общая проблема: построение составных объектов (composability) // Небезопасный объект! public class Employees { Set Общая проблема: построение составных объектов (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 Решение: Software Transactional Memory (STM) • Классические транзакции: - Atomicity - Consistency - Isolation - Durability

Поддержка на уровне языка // если бы можно было бы написать так… public boolean Поддержка на уровне языка // если бы можно было бы написать так… 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) Без поддержки на уровне языка // придется написать так… public boolean contains(final Employee e) { return Transaction. atomic( new Atomic. Block() { public Boolean call() { return working. contains(e) || vacating. contains(e); } }); } // и т. п.

Transaction public class Transaction { public static <R> R atomic(Atomic. Block<R> call) { for Transaction public class Transaction { public static R atomic(Atomic. Block call) { for (; ; ) { Transaction t = begin. Transaction(); try { R result = call(); if (t. commit()) return result; } catch (Runtime. Exception|Error e) { t. rollback(); throw e; } } // далее идет метод begin и т. п.

Реализация транзакций public class TVar<T> { private T value; public T get() { // Реализация транзакций public class TVar { private T value; public T get() { // что-то здесь… } public void set(T value) { // что-то тут… } } // Хотим чтобы в рамках транзакции при использовании операций чтения-записи всегда получалось линеаризуемое исполнение.

Реализация транзакций с блокировками • Двухфазная блокировка (2 PL = 2 Phase Locking) - Реализация транзакций с блокировками • Двухфазная блокировка (2 PL = 2 Phase Locking) - Все конфликтующие операции защищаются локами, исключающими конфликты. - В начале транзакции (первая фаза) локи накапливаются. - В конце транзакции (вторая фаза) локи освобождаются • Основная теорема: Любое допустимое исполнение такой системы будет линеаризуемо.

Реализация транзакций с блокировками // В классе TVar добавляем… private final Read. Write. Lock Реализация транзакций с блокировками // В классе 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<Transaction> Реализация транзакций с блокировками // В классе Transaction добавляем… private static final Thread. Local CURRENT= new Thread. Local(); private final List locks = new Array. List(); public static Transaction begin. Transaction() { Transaction t = new Transaction(); CURRENT. set(t); return t; } public static Transaction current. Transaction() { return CURRENT. get(); } void add. Lock(Lock lock) { locks. add(lock); }

сommit с блокировками // В классе Transaction добавляем… public boolean commit() { for (Lock с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) { 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<TVar<? >> writes rollback с блокировками 2 // В классе Transaction добавляем… private final Set> writes = new Hash. Set>(); public void add. Write(TVar var) { writes. add(var); } public void rollback() { for (TVar var : writes) var. rollback(); for (Lock lock : locks) lock. unlock(); }

Реализация транзакций с блокировками • Проблемы – прогресс - Взаимные блокировки (deadlocks) • Нужно Реализация транзакций с блокировками • Проблемы – прогресс - Взаимные блокировки (deadlocks) • Нужно написать - Предотвращение взаимных блокировок (deadlock avoidance) - Определение взаимных блокировок (deadlock detection) • Например, по истечению времени ожидания лока - Устранение взаимных блокировок (deadlock resolution) • Откатывая и начиная снова одну из заблокированных транзакций

Реализация транзакций без блокировок public class Transaction { private static final int ACTIVE = Реализация транзакций без блокировок 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<T> { final Transaction Хранение значений без блокировок // Реализуем вспомогательный объект class Var. Holder { final Transaction owner; final Object value; Object new. Value; // updated by owner Var. Holder(Transaction owner, Object value) { this. owner = owner; this. value = value; this. new. Value = value; } // Текущее значение зависит от состояния транзакции T current() { return owner. is. Committed() ? (T)new. Value : (T)value; }

 «Открытие» переменной без блокировок public class TVar<T> { private Atomic. Reference<Var. Holder<T>> holder «Открытие» переменной без блокировок public class TVar { private Atomic. Reference> holder = … // Будем «открывать» переменную перед любым доступом Var. Holder open() { Transaction tx = Transaction. current(); Var. Holder old, upd; do { old = holder. get(); if (old. owner == tx) return old; old. owner. rollback(); // если активен! upd = new Var. Holder(tx, old. current()); } while (!holder. compare. And. Set(old, upd)); return upd; }

Доступ к переменной без блокировок // В классе TVar<T> public T get() { return Доступ к переменной без блокировок // В классе TVar public T get() { return (T)open(). new. Value; } public void set(T value) { open(). new. Value = value; }

Реализация транзакций без блокировок • Это реализация без помех (obstruction-free) - Разные потоки могут Реализация транзакций без блокировок • Это реализация без помех (obstruction-free) - Разные потоки могут бесконечно долго другу мешать закончить транзакцию без прогресса - Но если активен только один поток, то прогресс гарантирован • Сравните с версией с блокировками - На практике, нужно управлять конфликтами (contention manager), чтобы отслеживать конфликты и увеличивать прогресс • Проблема - Даже читающие транзакции мешают другу • Предыдущий алгоритм с блокировками был здесь лучше

Параллельное чтение без блокировок // В классе TVar не будем открывать при чтении public Параллельное чтение без блокировок // В классе TVar не будем открывать при чтении public T get() { return holder. get(). current(); } • Проблема: Значение может поменяться в процессе работы транзакции (non-repeatable read) - А значит, нет линеаризуемости транзакций • Решения: - Перепроверка корректности во время завершения транзакции - Многоверсионный контроль корректности (MVCC – Multiversion Concurrency Control)

Перепроверка корректности во время завершения транзакции - read // В классе Transaction запомним прочитанные Перепроверка корректности во время завершения транзакции - read // В классе Transaction запомним прочитанные значения private final Map, Object> reads = new Hash. Map, Object>(); // Метод для чтения значения в транзакции T read(TVar var) { Var. Holder holder = var. holder. get(); T cur = holder. current(); if (holder. owner == this) return cur; if (reads. contains. Key(var)&&reads. get(var)!=cur) rollback(); // кто-то поменял в процессе… if (state. get() == ABORTED) throw new Rollback(); reads. put(var, cur); return cur; }

Перепроверка корректности во время завершения транзакции - commit // В классе Transaction будем открывать Перепроверка корректности во время завершения транзакции - commit // В классе Transaction будем открывать прочитанные // переменные перед самым завершением транзакции public boolean commit() { for (Map. Entry, Object> entry : reads. entry. Set()) { Var. Holder cur = entry. get. Key(). open(); Object expect = entry. get. Value(); if (cur. value != expect) { rollback(); return false; } } return state. compare. And. Set(ACTIVE, COMMITTED); }

Перепроверка корректности во время завершения транзакции - get // В классе TVar будем использовать Перепроверка корректности во время завершения транзакции - 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 будем присваивать номер // Следующий Версионность значений – нумерация транзакций // В классе 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, Long> reads = new Hash. Map, Long>();

Версионность значений – версия изменений // В классе Var. Holder будем хранить final Transaction Версионность значений – версия изменений // В классе 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 <T> T read(TVar<T> var) { Var. Версионность значений – чтение // В классе Transaction T read(TVar var) { Var. Holder holder = var. holder. get(); if (holder. owner. sequence > sequence) rollback(); if (holder. owner == this)return(T)holder. new. Value; long version; T cur; // читаем пару (версия, знач) if (holder. owner. is. Committed()) { version = holder. owner. state. get(); cur = (T)holder. new. Value; } else { version = holder. version; cur = (T)holder. value; } if (reads. contains. Key(var) && reads. get(var) != version) rollback(); if (state. get() == ABORTED) throw new Rollback(); reads. put(var, version); return cur; }

Версионность значений – проверка // В классе Transaction public boolean commit() { for (Map. Версионность значений – проверка // В классе Transaction public boolean commit() { for (Map. Entry, Long> entry : reads. entry. Set()) { Var. Holder cur = entry. get. Key(). open(); if (cur. version != entry. get. Value()) { rollback(); return false; } } // здесь надо атомарно поменять state и // передвинуть SEQUENCE на следующий … }