Просто о RCU (Read–Copy-Update). Часть 2

от автора

Введение

Продолжим тему использования механизма RCU в системном программировании Linux. В первой части мы создали примитивный цикл неблокирующего изменения одной переменной, в данной части разберем пример неблокирующего изменения списка значений.

Библиотека Liburcu

В тесте используем библиотеку liburcu. Данная библиотека предоставляет как примитивы RCU так и работу с полноценными структурами, такими как хеш-таблицы, очереди, стеки и списки.

Подробное описание https://liburcu.org/

Для нашего сегодняшнего теста выбрана работа с динамическим RCU списком.

https://lwn.net/Articles/573441/.

Описание задачи

Поскольку основной смысл применения RCU это задачи с одним писателем и множеством читателей, попробуем реализовать следующий пример. Дан динамический список t_list в который необходимо добавлять и удалять записи, без блокировок чтения и нарушения целостности данных. Каждый элемент списка будет указывать на структуру node из структуры t_node в которой поля node и rcu_head необходимы для работы RCU, а value является пользовательскими данными.

CDS_LIST_HEAD(t_list);  struct t_node {     long long value;     struct cds_list_head node;     struct rcu_head rcu_head; }; 

Для работы со списком формируем пул потоков из которых, один является писателем и определяется функцией void *test_l_add(void *arg) , а остальные будут читателями с функцией void *test_l_get(void *arg).

    for (i = 0; i < num_threads; i++) {         if (i < 1)             pthread_create(&tid[i], NULL, &test_l_add, (void *) i);         else             pthread_create(&tid[i], NULL, &test_l_get, (void *) i);     }

После запуска нитей, основной поток ожидает 5 секунд, разблокирует Mutex остановки и ждет завершения всех дочерних нитей. Затем освобождение памяти и завершение процесса.

    sleep(5);     pthread_mutex_unlock(&m1);      for (i = 0; i < num_threads; i++) {         pthread_join(tid[i], NULL);     }

Писатель RCU

Функция писателя void *test_l_add(void *arg) выполняет бесконечный цикл в ожидании освобождения Mutex. В цикле выполняется имитация работы со списком, при каждой итерации цикла добавляются 2 элемента списка со значениями REZ1 и REZ2. Функция cds_list_add_tail_rcu выполняется вне секции rcu_read_lock и не блокирует читателей. Каждый читатель, пока находится в rcu_read_lock, будет видеть старую версию списка.

        struct t_node *node = malloc(sizeof(*node));         if (node) {             node->value = REZ1;             cds_list_add_tail_rcu(&node->node, &t_list);             count++;         }

На каждый сотый цикл выполняется поиск и удаление всех элементов со значением REZ2. В данном фрагменте реализован механизм отложенного удаления ресурсов, после вызова функции cds_list_del_rcu вызывается функция call_rcu которой передается в качестве параметра имя функции для освобождения ресурсов.

        if (count1 % 100 == 0) {             rcu_read_lock();             struct t_node *node = NULL;             cds_list_for_each_entry_rcu(node, &t_list, node)             {                 if (node->value == REZ2) {                     cds_list_del_rcu(&node->node);                     call_rcu(&node->rcu_head, free_node_rcu);                 }             }             rcu_read_unlock();         }

Функция освобождения ресурсов , вызывается в момент когда все нити читатели данного ресурса покинули секцию rcu_read_lock. Это очень важный момент, мы не вызываем synchronize_rcu() а поручаем библиотеке освободить ресурс когда это станет возможным, тем самым не блокируем писателя.

static void free_node_rcu(struct rcu_head *head) {     struct t_node *node = caa_container_of(head, struct t_node, rcu_head);     node->value = 0;     free(node); } 

Читатель RCU

Функция читателя void *test_l_get(void *arg) , также в бесконечном цикле выполняет чтение всего списка с проверкой корректности значений value. Чтение списка происходит в в секции rcu_read_lock(). Макрос cds_list_for_each_entry_rcu выполняет просмотр всего списка, возвращая указатель на структуру t_node. При освобождении Mutex нить завершает работу и выводит на экран количество циклов чтения и количество считанных элементов в последнем цикле. Необходимо обратить внимание что rcu_read_lock() и rcu_read_unlock() всегда парные , не допускайте незакрытую секцию чтения.

    while (pthread_mutex_trylock(&m1) == EBUSY) {         rcu_read_lock();         struct t_node *node = NULL;         count_elm = 0;         cds_list_for_each_entry_rcu(node, &t_list, node)         {             if (node->value != REZ1 && node->value != REZ2) {                 printf("Fatal error :%lld :%lld :%lld\n", i, count, count_elm);                 exit(-1);             }             count_elm++;         }         count++;         rcu_read_unlock();     }     printf("End GET :%lld :%lld :%lld\n", i, count, count_elm);

Тестирование

Приступаем к тестированию основной задачи — показать что тестовое приложение работает эффективно в независимости от числа нитей читателей.

Первый запуск выполним для проверки холостой работы писателя. Установим количество нитей = 1. Результатом работы являются строка Start и End , которые показывают номер нити и количество циклов. List empty выдает информацию сколько в нашем списке значений value равно REZ1 и REZ2 соответственно. Результаты постоянно изменяются +/- тысяча при 350 тысячах циклов за 5 секунд это нормально. Итак примем что моих условиях одна нить совершает ~340-360 тысяч циклов.

Start ADD :0
End ADD :0 :356500
List empty 356500 1

Писателям без читателей скучно, добавим 1. Появилась запись End GET читатель выполнил 3947 циклов и в последнем цикле в списке было 359499 элементов.

Start ADD :0
Start GET :1
End GET :1 :3947 :359499
End ADD :0 :359440
List empty 359440 41

Добавим сразу до 4.

Start ADD :0
Start GET :2
Start GET :1
Start GET :3
Start GET :4
End GET :1 :4567 :364999
End ADD :0 :364977
End GET :4 :3669 :365055
End GET :2 :4175 :365055
End GET :3 :3794 :365055
List empty 364977 78

И до 12, больше чем количество ядер особого смысла нет.

Start ADD :0
Start GET :3
Start GET :2
Start GET :1
Start GET :4
Start GET :11
Start GET :10
Start GET :5
Start GET :7
Start GET :9
Start GET :6
Start GET :8
End GET :10 :4101 :387799
End GET :1 :4414 :387899
End GET :6 :3699 :387999
End GET :2 :4140 :387999
End ADD :0 :387974
End GET :3 :4957 :388049
End GET :4 :3901 :388049
End GET :5 :3550 :388049
End GET :7 :3999 :388049
End GET :11 :3917 :388049
End GET :8 :3637 :388049
End GET :9 :3755 :388049
List empty 387974 75

По результатам прошу обратить внимание что каждая из нитей останавливается со своим значением количество элементов, до тех пор пока не остановилась нить-писатель, далее все читатели считали зафиксированный результат 387974+75 и остановились.

И каков итог измерений? RCU работает и очень эффективно, важно только правильно применить.

Вывод

Вполне уверенно можно сказать что тестовый проект получился рабочий, эффективный и простой в понимании. Надеюсь данный материал будет полезен людям, изучающим системное программирование в Linux.

Код проекта https://github.com/vic-35/test_liburcu2 , проект создан в QT creator для облегчения отладки и запуска.


ссылка на оригинал статьи https://habr.com/ru/post/715852/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *