Практика Lock-free. RealTime-сервер
-
Upload
platonov-sergey -
Category
Software
-
view
704 -
download
0
Transcript of Практика Lock-free. RealTime-сервер
Облаков Константин
Разработчик группы обработки свежего контента
Lock-free in practice:
RealTime-Server
• Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
• Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
• Mutex (global, grained, etc.)
• Atomic memory transactions (hardware)
• Lock-free / wait-free
Многие ядра = синхронизация:
• Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
1.Свежесть бывает только одна
2.Нужно доносить информацию консистентно
3.Производительность нужна всем
4.Время ответа ограничено
5.Операций чтения намного больше записи
RealTime-Server
• Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
• Самодостаточные lock-free структуры
слишком сложны и специфичны
• Куча лишних операций
• Консистентность всё равно отсутствует
Не надо так!
• Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
• Если данные только для чтения
• Если данные меняются атомарно
• Если данные в единоличном владении
Когда синхронизация не нужна
Read: Всё до чего можно добраться по
указателям – читаемо и консистентно
Copy: Перед изменением данные копируются
Update: Откопированные данные можно
обновлять
Но только один пишущий поток
Строим на основе этого систему
18
• Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
19
• Копировать память всего процесса – дорого!
• Каждый процесс не владеет памятью
напрямую – есть прослойка PageTable
• Если откопировать только прослойку –
будет видимость полного копирования
• Надо применять copy-on-write для страниц
Как работает системный вызов fork()
20
• В. дерево – переиспользуем поддеревья
• В. стэк – переиспользуем общую часть
• В. хэш – переиспользуем чанки – атомарно добавляем записи
• при открытой адресации сначала пишем данные, затем ключ и (опционально) флаг готовности, если ключ не атомарен
• при списковом разрешении коллизий используем атомарность указателей
– старые записи просто маркируем старыми, не удаляя
– при необходимости в запись добавляем номер версии хэша
– слишком заполненный хэш копируем в новое место
• Прочее …
Обобщение – версионные структуры
34
1.Надо регистрировать читателей!
2.Надо просматривать список читающих
3.Никем не читаемую не текущую версию
можно чистить
4.Если всё владение идёт через умные
указатели со счётчиком ссылок – удаление
подструктур произойдёт автомагически!
5.Схема накладывает свой отпечаток на
читающий поток - нужен цикл переопроса
Когда удалить старую версию?
36
Регистрация читателей
Зарезервированные ячейки
Счётчик занятых Нет запрета на
очистку старых
версий!
37
Регистрация читателей
Зарезервированные ячейки
Счётчик занятых Нет запрета на
очистку старых
версий!
38
Регистрация читателей
C
Зарезервированные ячейки
Счётчик занятых Версию C и
более поздние
удалять нельзя!
39
Регистрация читателей
D C
Зарезервированные ячейки
Счётчик занятых Версию C и
более поздние
удалять нельзя!
40
Регистрация читателей
D C
Зарезервированные ячейки
Счётчик занятых Версию C и
более поздние
удалять нельзя!
41
Регистрация читателей
D C F
Зарезервированные ячейки
Счётчик занятых Версию C и
более поздние
удалять нельзя!
42
Регистрация читателей
C F
Зарезервированные ячейки
Счётчик занятых Версию C и
более поздние
удалять нельзя!
43
Регистрация читателей
F
Зарезервированные ячейки
Счётчик занятых Версию F и
более поздние
удалять нельзя!
44
Регистрация читателей
G F
Зарезервированные ячейки
Счётчик занятых Версию F и
более поздние
удалять нельзя!
Переиспользуем ячейки в потоке!
45
1.Получаем ячейку, если пока нет
2.Читаем текущий указатель на версию
3.Записываем в свою ячейку
4.Снова читаем указатель
5.Если не совпал – на шаг 3
WARNING: Это не spin lock!
Если указатель меняется – в системе
происходит прогресс
Цикл переопроса при регистрации
46
• Храним порядковый номер версии (>0)
• В ячейке хранить не указатель, а номер
• Читатетель записывает номер версии в
ячейку, затем читает указатель на версию
и сразу работает с ней
Надо потребовать, что переполнения
счётчика или не поисходит, или происходит
за “достаточно большое время”
Как можно сделать wait-free
48
• Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
49
• “Документ” – набор 1000 случайных чисел
• “Поиск” – ищем количество уникальных
документов, содержащих число из
заданного диапазона [from, to)
• Запросы на добавление документов
• Запросы на поиск (не более 1000 тредов)
• Wait-free!
Условия примера RealTime-Server
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
Класс итератора по 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
Класс 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
Класс ноды дерева
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
Класс ноды дерева
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
Класс ноды дерева
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
Класс ноды дерева
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
Класс 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
Класс 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;
}
59
Класс RCU-дерева
void RCU_Tree::find_docs(
int from, int to, set<int>* docs)
{
if (Root) {
Root->find_docs(from, to, docs);
}
}
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
Класс очереди версий (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
Класс очереди версий (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
Класс очереди версий (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
Класс очереди версий (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
Класс очереди версий (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
Класс сессии на запись
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
Класс сессии на чтение
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
Использование сессии на запись
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
Использование сессии на чтение
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();
}
71
Замеряем время работы треда
0
5
10
15
20
25
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
Write
Read
Количество ядер
72
• Введение
• Мотивировка
• Традиционный подход к lock-free
• Read Copy Update
• Версионный подход
• Большой пример
• Заключение
План
73
• Lock-free позволяет достичь максимальной
производительности на многих ядрах
• Общие алгоритмы lock-free пока ещё сложны
• Конкретные подслучаи доступны для
реализации любому разработчику
• Предложенный подход позволяет легко
сделать lock-free RealTime-Server в модели
Writer + N x Reader
• Подход разграничивает Write / Read – можно
использовать message passing для Write и т.д.
Заключение
Константин Облаков
Разработчик группы
обработки свежего контента
Спасибо за
внимание!