Рассмотрены известные автору подходы к реализации как lock-free, так и fine-grained lock-based set/map: хеш-таблицы, деревья. Что из подходов STL может быть реализовано в lock-free манере, а что принципиально нет. Подводные камни lock-free и их нейтрализация.
2. Concurrent mapsConcurrent maps
LIBCDS
Максим Хижинский, C++ Russia 2015
Операции:
● insert( key, value )
●
erase( key )
● find( key )
План:
● Concurrent maps изнутри
●
Управление памятью в lock-free структурах
●
Lock-free ordered list и concurrent maps на его основе
● Немного о деревьях
3. Hash tableHash table
LIBCDS
Максим Хижинский, C++ Russia 2015
0 1 2 3 4 5 6
X X X
k1
k
k
k k
k
kk
k
k
2
3
4 5
6
7
8
9
T[7]
T[i] = std::hash( key ) % 7
Список
коллизий
4. Striped mapStriped map
LIBCDS
Максим Хижинский, C++ Russia 2015
0 1 2 3 4 5 6
X X X
k1
k
k
k k
k
kk
k
k
2
3
4 5
6
7
8
9
T[7]
T[i] = std::hash( key ) % 7
5. Striped mapStriped map
LIBCDS
Максим Хижинский, C++ Russia 2015
0 1 2 3 4 5 6
X X X
k1
k
k
k k
k
kk
k
k
2
3
4 5
6
7
8
9
T[7]
T[i] = std::hash( key ) % 7
Lock[7]
6. Striped map: rehashStriped map: rehash
LIBCDS
Максим Хижинский, C++ Russia 2015
1. for ( i = 0; i < L; i++ )
Lock[i].lock();
2. N := 2 * N;
3. rehash T
4. for ( i := L - 1; i >=0; --i )
Lock[i].unlock();
7. L — const
Initial: L = N
Rehash: N := 2N
Striped map: rehashStriped map: rehash
LIBCDS
Максим Хижинский, C++ Russia 2015
0 1 2 3 4 5 6
X X
k1
k
k
k k kkk
kk
2
3
4 5 67
89
T[8]
Lock[i] = std::hash( key ) % 4
Lock[4]
7
T[j] = std::hash( key ) % 8
9. Lock-free hash tableLock-free hash table
LIBCDS
Максим Хижинский, C++ Russia 2015
0 1 2 3 4 5 6
X X X
k1
k
k
k k
k
kk
k
k
2
3
4 5
6
7
8
9
T[8]
Lock-free
список
коллизий
7
k
10
10. Lock-free ordered listLock-free ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
Операции:
●
insert( node )
● erase( key )
●
find( key )
template <class T>
struct node {
std::atomic<node*> next_;
T data_;
};
H T52 8
Lock-free примитивы:
● atomic load/store
●
atomic compare-and-swap (CAS)
11. CAS — compare-and-swapCAS — compare-and-swap
LIBCDS
Максим Хижинский, C++ Russia 2015
template <typename T>
bool CAS( T * pAtomic, T expected, T desired )
atomically {
if ( *pAtomic == expected ) {
*pAtomic = desired;
return true;
}
else
return false;
};
12. Lock-free list: insertLock-free list: insert
LIBCDS
Максим Хижинский, C++ Russia 2015
H T52 8
3
H T52 8
3
3. prev->next_.CAS( next, new_node )
1. find insert position for key 3
2. new_node.next_.store( next )
H T52 8
prev next
new_node
13. Lock-free list: eraseLock-free list: erase
LIBCDS
Максим Хижинский, C++ Russia 2015
1. find key 3
2. prev->next_.CAS( found, next )
H T52 8
prev
3
found
H T52 83
Проблема: параллельный insert
next
14. Lock-free list: insert/eraseLock-free list: insert/erase
LIBCDS
Максим Хижинский, C++ Russia 2015
A: find key 3
H T52 8
prev
3
found
B: find insert pos for key 4
iprev inext
A: erase key 3
H T52 83
prev->next_.CAS( found, next )
next
B: insert key 4
H T52 83
4
iprev->next_.CAS( inext, new_item )
local vars
15. Marked pointerMarked pointer
LIBCDS
Максим Хижинский, C++ Russia 2015
[ T.Harris, 2001 ]
Двухфазное удаление:
● Логическое удаление — помечаем элемент
●
Физическое удаление — исключаем элемент
В качестве метки используем младший бит указателя
16. Lock-free list: marked pointerLock-free list: marked pointer
LIBCDS
Максим Хижинский, C++ Russia 2015
H T52 8
prev
3
found
iprev inext
nextA: erase
B: insert
A: Logical deletion - mark item found
H T52 83
found->next_.CAS( next, next | 1 )
B: iprev->next_.CAS( inext, new_item ) - failed!!!
A: Physical deletion - remove item found
H T52 83
prev->next_.CAS( found, next )
17. Lock-free list: problemsLock-free list: problems
LIBCDS
Максим Хижинский, C++ Russia 2015
H T52 8
prev
3
found
iprev inext
nextA: erase
B: insert
iprev->next_.CAS( inext, new_item )
prev->next_.CAS( found, next )
local vars
Вдруг уже удалены?..
18. Lock-free list: problemsLock-free list: problems
LIBCDS
Максим Хижинский, C++ Russia 2015
Проблемы:
●
Защита локальных данных — когда элемент можно
безопасно удалить?
●
ABA-проблема
19. ABA-проблемаABA-проблема
LIBCDS
Максим Хижинский, C++ Russia 2015
52
prev
3
found next
Thread A: erase(3) Thread B
52 3
erase(3); erase(5)
2 3
insert(4)
Heap
new node(4) alloc
delete
42
preempted...
42
prev found next
5
addr(3) == addr(4)
prev->next_.CAS( found, next ) - success!!!
2 мусор
20. SMRSMR
LIBCDS
Максим Хижинский, C++ Russia 2015
Проблемы:
● Защита локальных данных — когда элемент можно
безопасно удалить?
●
ABA-проблема
Решение:
Safe memory reclamation (SMR)
● Tagged pointers
●
Hazard Pointers
●
User-space RCU
21. Tagged pointersTagged pointers
LIBCDS
Максим Хижинский, C++ Russia 2015
pointer tag
prev->next_.dwCAS( found, <next.ptr, prev->next_.tag + 1> )
template <class T>
struct tagged_ptr {
T * ptr;
uintptr_t tag;
};
Требует dwCAS — не везде есть
Решает только ABA-проблему
Освободить память нельзя,
нужен free-list
[ boost.lock-free ]
H T52 8
prev
3
found next
22. Tagged pointers: historyTagged pointers: history
LIBCDS
Максим Хижинский, C++ Russia 2015
ABA-проблема характерна только для CAS
Архитектуры процессоров
LL/SC:
●
IBM PowerPC
● MIPS
●
ARM
➢ LL — load linked
➢ SC — store conditional
bool weak_CAS( T * ptr,
T expected, T desired )
{ T cur = LL( ptr );
return cur == expected
&& SC( ptr, desired );
}
Эмуляция LL/SC на CAS
— намного труднее
CAS:
●
x86, amd64
● Sparc
●
Itanium
С++11 — только CAS
23. Hazard pointersHazard pointers
LIBCDS
Максим Хижинский, C++ Russia 2015
✔ Использует только атомарные чтение/запись
Защищает только локальные ссылки
✔ Размер массива отложенных (готовых к
удалению) элементов ограничен сверху
Перед работой с указателем его следует
объявить как hazard
решает ABA-проблему
Физическое удаление элементов
24. Hazard pointersHazard pointers
LIBCDS
Максим Хижинский, C++ Russia 2015
H T52 8
prev
3
found next
erase( Key k ) {
hp_guard h1 = get_guard();
hp_guard h2 = get_guard();
retry:
node * prev = Head;
do {
node * found = h2.protect( prev->next_);
if ( found->key == k )
if (prev->next_.CAS( found, found->next_)) {
hp_retire( found );
return true;
}
else
goto retry;
h1 = h2;
prev = found;
} while ( found->key < k );
return false;
}
Распределяем HP (TLS)
Защищаем элемент
Удаляем элемент
25. Hazard pointersHazard pointers
LIBCDS
Максим Хижинский, C++ Russia 2015
P – thread count
Thread 0
Thread HP Manager
0
1
…
K - 1
HP[K]
0
1
2
…
R - 1
Retired[R]
Hazard Pointer Singleton
Thread
1
Thread
P - 1
K = 4 R = 2 KP
<K,P, R> : R > K * P
26. Hazard PointersHazard Pointers
LIBCDS
Максим Хижинский, C++ Russia 2015
Объявление Hazard Pointer'а – защита локальной ссылки
class hp_guard {
void * hp;
// ...
};
T * hp_guard::protect(
std::atomic<T*>& what) {
T * t;
do {
hp = t = what.load(std::memory_order_relaxed);
} while (t != what.load(std::memory_order_acquire));
return t;
}
0
1
…
K - 1
HP[K]
27. Hazard PointersHazard Pointers
LIBCDS
Максим Хижинский, C++ Russia 2015
Удаление элемента
void hp_retire( T * what ) {
push what to current_thread.Retired array
if ( current_thread.Retired is full )
hp.Scan( current_thread );
}
void hp::Scan() {
void * guarded[K*P] = union HP[K] for all P thread;
foreach ( p in current_thread.Retired[R] )
if ( p not in guarded[] )
delete p;
}
<K,P, R> : R > K * P
0
1
2
…
R - 1
Retired[R]
0
1
…
K - 1
HP[K]
28. User-space Read-Copy UpdateUser-space Read-Copy Update
LIBCDS
Максим Хижинский, C++ Russia 2015
✔ RCU — метод синхронизации:
RCU.lock() / RCU.unlock()
✔ Разработан для почти-read-only данных (map, set)
✔ Очень легкие read-side lock
✔ Удаление элемента — ожидание окончания эпохи
решает ABA-проблему
Физическое удаление элементов
RCU.lock(): ничего не блокирует
Объявляет, что поток входит в
текущую RCU-эпоху
30. Lock-free hash tableLock-free hash table
LIBCDS
Максим Хижинский, C++ Russia 2015
0 1 2 3 4 5 6
X X X
k1
k
k
k k
k
kk
k
k
2
3
4 5
6
7
8
9
T[8]
Lock-free
cписок:
HP/RCU
+ marked
pointers
7
k
10
Hash table + Lock-free ordered list
No rehashing
31. Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
Rehashing приводит к перераспределению
элементов между buckets
0 1
2
4
6
3
5
0 1 2 3
2 354
6
Key % 2 Key % 4
Вместо того, чтобы перемещать
элементы между buckets, будем
перемещать buckets между
элементами
[ Nir Shavit, 2003 ]
32. Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
0 8 2 1 9 13
T[0]
T[1]
k iSentinel node Regular node
N = 2 size() = 4
Load factor L: size() / N ≤ L
Если L = 2, то вставка нового элемента приводит к увеличению
hash table
Hash table
Lock-free ordered list
33. Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
0 8 2 1 9 13
T[0]
T[1]
k iSentinel node Regular node
N = 4
Hash table
Insert
T[2] = null
T[3] = null
10
T[i]: i = hash % N
10 % 4 = 2 — T[2] не инициализирован
Нужно вставить новый sentinel node
Куда вставлять?..
2
10
34. Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
0 8 2 1 9 13
T[0]
T[1]
k iSentinel node Regular node
N = 4
Hash table
Insert
T[2] = null
T[3] = null
10
2
Split bucket
Bucket = hash % 2
m + 1
Parent_bucket Parent_bucket + 2
m
= 10 % 4
10 % 4, m = 12
Parent_bucket = 0
35. Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
0 8 2 1 9 13
T[0]
T[1]
k iSentinel node Regular node
N = 4
Hash table
Insert
T[2] = null
T[3] = null
10
2
Split bucket
Bucket = hash % 2
m + 1
Parent_bucket Parent_bucket + 2
m
= 10 % 4
10 % 4, m = 12
Parent_bucket = 0
0000 1000 0010 0001 1001 1101
0010
Арифметика
по модулю
2
p
36. Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
0 8 2 1 9 13
T[0]
T[1]
k iSentinel node Regular node
N = 4
Hash table
Insert
T[2] = null
T[3] = null
10
2
Split bucket
Bucket = hash % 2
m + 1
Parent_bucket Parent_bucket + 2
m
= 10 % 4
10 % 4, m = 12
Parent_bucket = 0
0000 0001 0100 1000 1001 1011
0100
Инвертируем
порядок бит!
Отсортирован!
37. Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
template <class Key, class T>
struct split_list_node
{
Key key;
uint key_hash; // = std::hash(key)
uint shah; // = invert( key_hash ) - для сортировки
T data;
};
38. Уже есть!!!
parent_bucket
Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
0 8 2 1 9 13
T[0]
T[1]
k iSentinel node Regular node
Hash table
Insert
T[2] = null
T[3] = null
2
0000 0 0001 1 0100 1 1000 0 1001 1 1011 1
Надо различать sentinel и
regular node.
msb(key_hash) = 1 — regular → lsb( shah ) = 1
msb(key_hash) = 0 — sentinel →lsb( shah ) = 0
0100 0
41. Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
Удаление — как из обычного lock-free списка
Sentinel node никогда не удаляются
1: Logical deletion - mark item
52 83
found->next_.CAS( next, next | 1 )
2: Physical deletion - unlink item
52 83
43. Lock-free ordered listLock-free ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
X10 15 23 34 5542
Complexity O(N)
Что ещё можно сделать из lock-free ordered list?
✔ Простой hash map — без расширения
✔ Split-ordered list
find(34)
47. Skip listSkip list
LIBCDS
Максим Хижинский, C++ Russia 2015
X
X
X
X
X
X
X
X10 15 23 34 5542
23
Tower
h = 3
Вероятностная структура данных:
P[ h == 1 ] = 1/2
P[ h == k ] = 1/2
k
, 0 < k < 32
h = lsb( rand() )
O(log(N))
48. Skip list: insertSkip list: insert
LIBCDS
Максим Хижинский, C++ Russia 2015
X
X
X
X
X
X
X
X10 15 23 34 5542
Head Tail
Поиск позиции: формируем массив prev[]
49. Skip list: insertSkip list: insert
LIBCDS
Максим Хижинский, C++ Russia 2015
X
X
X
X
X
X
X
X10 15 23 34 5542
Head Tail
Связываем в список на уровне 0
50. Skip list: insertSkip list: insert
LIBCDS
Максим Хижинский, C++ Russia 2015
X
X
X
X
X
X
X
X10 15 23 34 5542
Head Tail
Связываем в список на уровне 1, 2, …
снизу вверх
51. Skip list: eraseSkip list: erase
LIBCDS
Максим Хижинский, C++ Russia 2015
X
X
X
X
X
X
X
X10 15 23 34 5542
Head Tail
Двухфазное: 1. Logical deletion — mark
сверху вниз
52. Skip list: eraseSkip list: erase
LIBCDS
Максим Хижинский, C++ Russia 2015
X
X
X
X
X
X
X
X10 15 23 34 5542
Head Tail
Фаза 2. Physical deletion — unlink
сверху вниз
53. Skip listSkip list
LIBCDS
Максим Хижинский, C++ Russia 2015
Особенности:
Отсортированный контейнер
get_max(), get_min()
Можно использовать как priority queue
Сложность поиска: O(log N)
Hazard Pointer: для max height = 32 требует
min 64 hazard pointer'ов
Требует эффективной реализации tower
54. Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
«Прямой» lock-free подход приводит к очень сложным
алгоритмам.
Non-blocking подход дает менее сложные алгоритмы
Routing nodes - только ключи
Leaf nodes - данные
Leaf-oriented tree
Основная цель: lock-free / wait-free find()
55. Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
10
5 20
3015
27 45
Thread A: erase(15)
Thread B: erase(27)
10
5 20
3015
27 45
Error!!!
Достижимый
узел
Конкурентное CAS-based удаление
CAS
CAS
Должно быть
57. Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
Routing node
Left child Right child
State
Key
IFlag DFlag
MarkClean (default)
60. Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
10
5 20
3015
27 45
Thread A: erase(15)
Thread B: erase(27)
10
5 20
3015
27 45
Error!!!
Достижимый
узел
Конкурентное CAS-based удаление
CAS
CAS
Должно быть
61. Конкуренция
Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
10
5 20
3015
27 45
Thread A: erase(15)
Thread B: erase(27)
Конкурентное CAS-based удаление
A
BA
10
5 20
3015
27 45
A
A
Выиграл A
10
5 20
3015
27 45
A
Выиграл B
B
B
B: повтор
поиска 27
A: unflag +
повтор
поиска 15
62. Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
find() - lock-free
insert(), erase() - non-blocked
Характеристики:
Сложность:
O(log(N)) — для случайных ключей
O(N) — в худшем случае