Про RWLock (Задача о читателях и писателях)
Подробно - в википедии Задача о читателях-писателях — Википедия
Вкратце:
У вас есть, к примеру, список длинный, с данными какими-то. И несколько задач, которые, скажем, каждую секунду бегают по этим спискам и читают данные из них.
А раз в час появляется задача-писатель, которая, например, удаляет или вставляет элементы в список.
Рано или поздно такая софтина повиснет, потому, что очередной читатель, идя по списку, внезапно обнаружит,
что ->next указывает уже в никуда
Можно было бы обойтись мутексом - захотел доступ - захвати мутекс! Но при таком подходе только один читатель или писатель будет иметь доступ,
остальные же будут ждать.
Решают данную проблему специальным объектом синхронизации, который называется rwlock (Readers-Writers Lock)
Его можно “захватывать” для чтения сколько угодно раз из разных задач, но на запись - только одной задачей
По какой-то необъяснимой причине во FreeRTOS нет такого примитива, как rwlock. В их мейллисте люди выражают удивление такому факту (2025 на дворе),
но бородатые авторы FreeRTOS отказываются реализовывать. Сами, говорят, делайте.
Ну и сделаем.
Для FreeRTOS я написал rwlock (приоритет - писателям).
Он простой и быстрый, но скорее всего не соберется компилятором Си++.
А может и соберется.
Кто его знает.
PS:
Оказывается, в Си++ “все уже придумано до нас”, но только начиная с C17: std::shared_mutex
// Реализация RWLock для ESP32. На Си, на Си++ скорее всего не скомпилируется.
// Можно использовать как в среде Arduino, так и в среде ESP-IDF
//
// Инициализация:
// rwlock_t rw = RWLOCK_INIT;
//
// Задача-читатель:
// ....
// rw_lockr(&rw);
// do_something();
// rw_unlockr(&rw);
//
// Задача-писатель:
// ....
// rw_lockw(&rw);
// do_something();
// rw_unlockw(&rw);
#include <stdatomic.h>
#include <stdint.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
// Подсказки компилятору для предсказания ветвлений
#undef likely
#undef unlikely
#define unlikely(_X) __builtin_expect(!!(_X), 0)
#define likely(_X) __builtin_expect(!!(_X), 1)
// Двоичный семафор
#define lock(_Name) \
do { \
if (unlikely(_Name == NULL)) { \
_Name = xSemaphoreCreateBinary(); \
break; /* создан уже в заблокированном состоянии */\
} \
if (likely(_Name != NULL)) \
while (xSemaphoreTake(_Name, portMAX_DELAY) == pdFALSE) { \
/* Прошло 1200 часов. Пробуем снова. */ \
} \
} while( 0 )
//
#define unlock(_Name) \
do { \
if (likely(_Name != NULL)) \
xSemaphoreGive(_Name); \
} while( 0 )
// Структура RWLock (блокировка чтения/записи)
typedef struct {
_Atomic uint32_t wreq; // Сколько запросов на запись ожидают своей очереди
_Atomic int cnt; // <0 — запись, 0 — свободно, >0 — чтение
SemaphoreHandle_t sem; // двоичный семафор, используется как блокирующий объект
} rwlock_t;
// Инициализатор для rwlock:
// rwlock_t my_lock = RWLOCK_INIT;
#define RWLOCK_INIT { 0, 0, NULL }
// Захват объекта "на запись".
//
// Если /уже есть/ активные читатели или писатели, функция блокируется (повисает) на /rw->sem/,
// а так же сигнализирует, что новые запросы на чтение пока не принимаются.
//
// Если нет ни читателей ни писателей, захватываем /rw->sem/ и устанавливаем /cnt/ в
// отрицательное значение, что означает «получена блокировка на запись».
//
void rw_lockw(rwlock_t *rw) {
// Ставим флаг «запрос на запись» до захвата /rw->sem/,
// чтобы новые читатели не успели влезть
rw->wreq++;
try_again:
// Пытаемся захватить основной синхронизирующий объект.
// Если он занят, ждём.
lock(rw->sem);
// Получили семафор, но проверим, не успел ли читатель таки влезть.
// Если успел — отпускаем и пробуем снова.
if (rw->cnt != 0) {
unlock(rw->sem);
vPortYield();
goto try_again;
}
rw->cnt = -1; // активный писатель
rw->wreq--;
}
// Освобождение блокировки на запись
// Ожидаем, что /cnt/ = -1. Если нет — ошибка в логике RWLock.
//
void rw_unlockw(rwlock_t *rw) {
rw->cnt = 0;
unlock(rw->sem);
}
// Захват блокировки на чтение.
// Если уже есть писатель или запрос на запись, ждём.
//
// Это основной, чаще всего используемый тип блокировки.
void rw_lockr(rwlock_t *rw) {
int cnt;
// Ждём, пока нет писателей и запросов на запись
while (atomic_load_explicit(&rw->cnt, memory_order_acquire) < 0 ||
atomic_load_explicit(&rw->wreq, memory_order_acquire) > 0)
vPortYield();
// Атомарно увеличиваем число читателей
cnt = atomic_fetch_add_explicit(&rw->cnt, 1, memory_order_acq_rel);
// Первый читатель захватывает /rw->sem/, чтобы писатели сразу блокировались.
// Если писатель всё же успел вклиниться, мы подождём, пока он завершит цикл try_again.
// Это вроде как баг, но он несущественный: просто один из читателей ошибочно будет опознан как писатель,
// ну и подождет, не рассыпется.
if (!cnt)
lock(rw->sem);
}
// Освобождение блокировки на чтение
//
void rw_unlockr(rwlock_t *rw) {
if (atomic_fetch_sub(&rw->cnt, 1) == 1)
unlock(rw->sem);
}