В докладе пойдёт речь о практическом применении lock-free структур и алгоритмов, которые используются в RealTime-Поиске в Яндексе. Из-за сложности теоретических исследований по lock-free и оторванности от практики часто создаётся впечатление, что это просто развлечение для знатоков computer science и не может быть использовано в реальном проекте. Будут рассмотрены проблемы традиционного подхода к lock-free и показано, как взглянув по новому на всю идею lock-free, добиться максимальной производительности, невозможной при использовании блокировок.
2. • Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
3. • Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
4. • Mutex (global, grained, etc.)
• Atomic memory transactions (hardware)
• Lock-free / wait-free
Многие ядра = синхронизация:
5. • Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
6. 1.Свежесть бывает только одна
2.Нужно доносить информацию консистентно
3.Производительность нужна всем
4.Время ответа ограничено
5.Операций чтения намного больше записи
RealTime-Server
7. • Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
13. • Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
14. • Если данные только для чтения
• Если данные меняются атомарно
• Если данные в единоличном владении
Когда синхронизация не нужна
15. Read: Всё до чего можно добраться по
указателям – читаемо и консистентно
Copy: Перед изменением данные копируются
Update: Откопированные данные можно
обновлять
Но только один пишущий поток
Строим на основе этого систему
18. 18
• Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
19. 19
• Копировать память всего процесса – дорого!
• Каждый процесс не владеет памятью
напрямую – есть прослойка PageTable
• Если откопировать только прослойку –
будет видимость полного копирования
• Надо применять copy-on-write для страниц
Как работает системный вызов fork()
20. 20
• В. дерево – переиспользуем поддеревья
• В. стэк – переиспользуем общую часть
• В. хэш – переиспользуем чанки
– атомарно добавляем записи
• при открытой адресации сначала пишем данные, затем ключ и (опционально)
флаг готовности, если ключ не атомарен
• при списковом разрешении коллизий используем атомарность указателей
– старые записи просто маркируем старыми, не удаляя
– при необходимости в запись добавляем номер версии хэша
– слишком заполненный хэш копируем в новое место
• Прочее …
Обобщение – версионные структуры
34. 34
1.Надо регистрировать читателей!
2.Надо просматривать список читающих
3.Никем не читаемую не текущую версию
можно чистить
4.Если всё владение идёт через умные
указатели со счётчиком ссылок – удаление
подструктур произойдёт автомагически!
5.Схема накладывает свой отпечаток на
читающий поток - нужен цикл переопроса
Когда удалить старую версию?
45. 45
1.Получаем ячейку, если пока нет
2.Читаем текущий указатель на версию
3.Записываем в свою ячейку
4.Снова читаем указатель
5.Если не совпал – на шаг 3
WARNING: Это не spin lock!
Если указатель меняется – в системе
происходит прогресс
Цикл переопроса при регистрации
46. 46
• Храним порядковый номер версии (>0)
• В ячейке хранить не указатель, а номер
• Читатетель записывает номер версии в
ячейку, затем читает указатель на версию
и сразу работает с ней
Надо потребовать, что переполнения
счётчика или не поисходит, или происходит
за “достаточно большое время”
Как можно сделать wait-free
48. 48
• Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
49. 49
• “Документ” – набор 1000 случайных чисел
• “Поиск” – ищем количество уникальных
документов, содержащих число из
заданного диапазона [from, to)
• Запросы на добавление документов
• Запросы на поиск (не более 1000 тредов)
• Wait-free!
Условия примера RealTime-Server
50. 50
Класс элемента RCU-стека
class Item {
int Data;
shared_ptr<Item> NextItem;
public:
Item(int data, shared_ptr<Item> nextItem)
: Data(data), NextItem(nextItem) { }
const Item* Next() const {
return NextItem.get();
}
int GetData() const {
return Data;
}
};
51. 51
Класс итератора по RCU-стеку
class const_iterator {
const Item* Current;
public:
const_iterator(const Item* current = nullptr)
: Current(current) { }
const_iterator& operator++() {
Current = Current->Next();
return *this;
}
bool operator!=(const const_iterator& other) const {
return Current != other.Current;
}
int operator*() const {
return Current->GetData();
}
};
52. 52
Класс RCU-стека
class RCU_Stack {
class Item;
shared_ptr<Item> First;
public:
void push(int data) {
shared_ptr<Item>(
new Item(data, First)).swap(First);
}
class const_iterator;
const_iterator begin() const {return First.get();}
const_iterator end() const { return nullptr; }
};
53. 53
Класс ноды дерева
class Node {
int Key;
shared_ptr<Node> Left;
shared_ptr<Node> Right;
RCU_Stack Stack;
public:
Node(int key, int data) : Key(key) {
Stack.push(data);
}
void add_node_data(int i, int data);
void find_docs(int from, int to, set<int>* docs);
static void make_copy(
shared_ptr<Node>& node
);
};
54. 54
Класс ноды дерева
void Node::add_node_data(int i, int data) {
if (i == Key) {
Stack.push(data);
} else if (i < Key && Left) {
make_copy(Left);
Left->add_node(i);
} else if (i > Key && Right) {
make_copy(Right);
Right->add_node(i);
} else {
shared_ptr<Node> new_child(new Node(i));
if (i < Key) Left = new_child;
else Right = new_child;
}
}
55. 55
Класс ноды дерева
void Node::find_docs(
int from, int to, set<int>* docs) {
if (from <= Key && Key < to) {
for (auto doc : Stack)
docs->insert(doc);
}
if (from < Key && Left) {
Left->find_docs(from, to, docs);
}
if (Key < to && Right) {
Right->find_docs(from, to, docs);
}
}
56. 56
Класс ноды дерева
void Node::make_copy(shared_ptr<Node>& node) {
if (node.use_count() > 1) {
shared_ptr<Node> new_node(new Node(*node));
node = new_node;
}
}
57. 57
Класс RCU-дерева
class RCU_Tree {
class Node;
shared_ptr<Node> Root;
public:
void add_node_data(int i, int data);
void find_docs(
int from, int to, set<int>* docs);
};
58. 58
Класс RCU-дерева
void RCU_Tree::add_node_data(int i, int data) {
Node::make_copy(Root);
if (Root) {
Root->add_node_data(i, data);
return;
}
shared_ptr<Node> new_root(
new Node(i, data));
Root = new_root;
}
61. 61
Класс очереди версий (part 1)
class Versions_queue {
using version_ptr = shared_ptr<Version>;
using version_info = pair<version_ptr, int>;
atomic<int> Client_id;
atomic<Version*> Current;
atomic<int> Current_version_id;
array<atomic<int>, 1000> Client_version_ids;
queue<version_info> Queue;
...
};
62. 62
Класс очереди версий (part 2)
class Versions_queue {
...
void cleanup_unused_versions() {
int max_client_id = Client_id;
auto begin = Client_version_ids.begin();
auto end = begin + max_client_id;
int min_ver_id = Current_version_id;
for (auto it = begin; it != end; ++it) {
int client_ver_id = *it;
if (client_ver_id && min_ver_id > cient_ver_id)
min_ver_id = client_ver_id;
}
while (!Queue.empty() &&
Queue.front().second < min_ver_id) Queue.pop();
}
...
};
63. 63
Класс очереди версий (part 3)
class Versions_queue {
...
public:
Versions_queue()
: Client_id(0), Current_version_id(1) {
for (auto& version : Client_version_ids)
version = 0;
version_ptr new_version(new Version);
Queue.push(new_version);
Current = new_version.get();
}
int get_client_id() {
return Client_id++;
}
...
};
64. 64
Класс очереди версий (part 4)
class Versions_queue {
...
public:
...
Version* get_current_version(int client_id) {
int version_id = Current_version_id;
Client_version_ids[client_id] = version_id;
return Current;
}
...
};
65. 65
Класс очереди версий (part 5)
class Versions_queue {
...
public:
...
Version* create_new_version() {
cleanup_unused_versions();
version_ptr ret(new Version(*Current));
Queue.push(
version_info(ret, Current_version_id + 1));
return ret.get();
}
...
};
66. 66
Класс очереди версий (part 6)
class Versions_queue {
...
public:
...
void release_current_version(int client_id) {
Client_version_ids[client_id] = 0;
}
void release_new_version(Version* version) {
Current = version;
++Current_version_id;
}
};
67. 67
Класс сессии на запись
class Write_session {
Versions_queue* Queue;
Version* New_version;
public:
Write_session(Versions_queue* queue)
: Queue(queue)
, New_version(Queue->create_new_version()) { }
~Write_session() {
Queue->release_new_version(New_version);
}
Version* operator->() {
return New_version;
}
};
68. 68
Класс сессии на чтение
class Read_session {
Versions_queue* Queue;
int Client_id;
Version* Current_version;
public:
Read_session(Versions_queue* queue, int client_id)
: Queue(queue)
, Client_id(client_id)
, Current_version(Queue->get_current_version(Client_id))
{ }
~Read_session() {
Queue->release_current_version(Client_id);
}
Version* operator->() {
return Current_version;
}
};
69. 69
Использование сессии на запись
for (int doc = 0; doc < 1000; ++doc) {
Write_session session(&Queue);
for (int j = 0; j < 1000; ++j) {
session->Tree.add_node_data(
random_number(), doc);
}
}
70. 70
Использование сессии на чтение
int sum = 0;
int client_id = Queue.get_client_id();
for (int i = 0; i < 7200; ++i) {
set<int> docs;
Read_session session(&Queue, client_id);
for (int j = 0; j < 1000; ++j) {
int from = random_number();
int to = from + 10;
session->Tree.check_node(from, to, &docs);
}
sum += docs.size();
}
72. 72
• Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
73. 73
• Lock-free позволяет достичь максимальной
производительности на многих ядрах
• Общие алгоритмы lock-free пока ещё сложны
• Конкретные подслучаи доступны для
реализации любому разработчику
• Предложенный подход позволяет легко
сделать lock-free RealTime-Server в модели
Writer + N x Reader
• Подход разграничивает Write / Read – можно
использовать message passing для Write и т.д.
Заключение