Практика lock-free. realtime-сервер

74
Облаков Константин Разработчик группы обработки свежего контента Lock-free in practice: RealTime-Server

Upload: platonov-sergey

Post on 08-Aug-2015

704 views

Category:

Software


0 download

TRANSCRIPT

Облаков Константин

Разработчик группы обработки свежего контента

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

• Версионный подход

• Большой пример

• Заключение

План

1.Найти lock-free реализацию всех структур

2.Заменить стандартные

Традиционный подход к lock-free

• Самодостаточные lock-free структуры

слишком сложны и специфичны

• Куча лишних операций

• Консистентность всё равно отсутствует

Не надо так!

Куча лишних операций:

ЯДРО

Контроль памяти

Регистрация действий

Циклы CAS

Куча лишних операций:

Общая консистентность отсутсвует:

Не связаны!

• Введение

• Мотивировка

• Традиционный подход к lock-free

• Read Copy Update

• Версионный подход

• Большой пример

• Заключение

План

• Если данные только для чтения

• Если данные меняются атомарно

• Если данные в единоличном владении

Когда синхронизация не нужна

Read: Всё до чего можно добраться по

указателям – читаемо и консистентно

Copy: Перед изменением данные копируются

Update: Откопированные данные можно

обновлять

Но только один пишущий поток

Строим на основе этого систему

16

- Копировать всё?

- Да!

17

- Это дорого!

- А вот и нет …

18

• Введение

• Мотивировка

• Традиционный подход к lock-free

• Read Copy Update

• Версионный подход

• Большой пример

• Заключение

План

19

• Копировать память всего процесса – дорого!

• Каждый процесс не владеет памятью

напрямую – есть прослойка PageTable

• Если откопировать только прослойку –

будет видимость полного копирования

• Надо применять copy-on-write для страниц

Как работает системный вызов fork()

20

• В. дерево – переиспользуем поддеревья

• В. стэк – переиспользуем общую часть

• В. хэш – переиспользуем чанки – атомарно добавляем записи

• при открытой адресации сначала пишем данные, затем ключ и (опционально) флаг готовности, если ключ не атомарен

• при списковом разрешении коллизий используем атомарность указателей

– старые записи просто маркируем старыми, не удаляя

– при необходимости в запись добавляем номер версии хэша

– слишком заполненный хэш копируем в новое место

• Прочее …

Обобщение – версионные структуры

21

Дерево

22

Дерево

23

Дерево

24

Стек

25

Стек

26

Стек

27

Хэш

28

Хэш

29

Хэш

30

Объединение структур

Старая версия Новая версия

на 99.9% те же

31

Указатель на корневую версию

Старая версия Новая версия

32

Указатель на корневую версию

Старая версия Новая версия

33

Указатель на корневую версию

Старая версия Новая версия

Когда удалить??

34

1.Надо регистрировать читателей!

2.Надо просматривать список читающих

3.Никем не читаемую не текущую версию

можно чистить

4.Если всё владение идёт через умные

указатели со счётчиком ссылок – удаление

подструктур произойдёт автомагически!

5.Схема накладывает свой отпечаток на

читающий поток - нужен цикл переопроса

Когда удалить старую версию?

35

Регистрация читателей

Зарезервированные ячейки

Счётчик занятых

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

47

1.Пишем в ячейку “пусто”

И всё!

Освобождение версии

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);

}

}

60

Класс версии

struct Version {

RCU_Tree Tree;

};

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 и т.д.

Заключение

Константин Облаков

Разработчик группы

обработки свежего контента

[email protected]

Спасибо за

внимание!