Представим себе скетч, многозадачный, в котором мы оперируем с какой-нибудь структурой, назовем ее
struct UserSettings {
...
int ref;
...
};
Или, для любителей Си++, пусть вместо структуры будет какой-нибудь класс, это не важно.
В структуре у нас есть член ref - счетчик активных “пользователей” данного экземпляра структуры, который используется для удаления (когда число пользователей становится равным нулю):
// Уменьшаем счетчик пользователей на единицу. Если счетчик равен нулю - то
// удаляем структуру
void
delete_user_settings(struct UserSettings *p) {
if (--p->ref == 0) {
free(p);
}
}
Так, вратце, работает логика со счетчиками ссылок (reference counters, refcounters): удаляем объект, когда на него нет ссылок, чтобы сохранить объект, вместо копирования мы просто увеличиваем счетчик ссылок.
Впрочем, код выше содержит одну принципиальную ошибку - доступ к ->ref из нескольких задач сразу приведет к тому, что счетчик будет принимать неверные значения.
Почему?
Ну если две задачи одновременно сделают ref++, то значение ref будет непредсказуемым: оно может увеличится на 1, на 2 или на 0. (А на некоторых архитектурах значение ref может быть вообще произвольным)
Что делать?
Ну, обычно применяют какой-нибудь объект синхронизации, например, мутекс или бинарный семафор:
// Уменьшаем счетчик пользователей на единицу. Если счетчик равен нулю - то
// удаляем структуру, исправленная версия:
void
delete_user_settings(struct UserSettings *p) {
mutex_acquire(p->mutex);
if (--p->ref == 0) {
mutex_release(p->mutex);
free(p);
return;
}
mutex_release(p->mutex);
}
И все прекрасно начинает работать (считаем, что p->mutex инициализирован где-то), но есть несколько проблем:
- На мутексе задача ,блокируется и ничего не делает, пока мутекс не разблокируется. Создается задержка.
- С мутексами существует проблема нарваться на deadlock.
- Если удалить задачу, которая захватила и не отпустила мутекс, то всё, привет.
- А еще - мутекс надо создавать и удалять. Ну, или хотя-бы создавать, бог с ним , с удалением.
Слава богу, на этих наших ардуинах многозадачных построенных на ESP32, например) компилятором поддерживается стандарт C11 с ключевым словом _Atomic и атомными операциями.
Такой подход называется “lockless programming”, про это довольно много написано в интернете. Современные ядра операционных систем так или иначе пользуются этим подходом, хотя, конечно, не весь код с мутексами может быть
сконвертирован в lockless код.
Код выше может быть переписан совсем без задержек* и использования объектов синхронизации следующим
образом:
#include <stdatomic.h>
struct UserSettings {
...
_Atomic int ref;
...
};
// Уменьшаем счетчик пользователей на единицу. Если счетчик равен нулю - то
// удаляем структуру, lockless версия:
void
delete_user_settings(struct UserSettings *p) {
if (atomic_fetch_sub(&p->ref, 1) == 1) { // atomic read-modify-store цикл
free(p);
return;
}
}
Ключевое слово _Atomic говорит компилятору о том, что наша переменная - особенная :).
В принципе, если теперь делать ref++ и ref–, то эти операции будут атомарными, с гарантированным результатом. Но так как нам еще нужно и атомарное сравнение, то мы используем atomic_fetch_sub(&p->ref,1). Эта конструкция вычитает единицу из счетчика и возвращает его значение до вычета.
C11 содержит множество атомарных операций, наподобие описанной выше: atomic_compare_exchange, atomic_fetch_add, atomic_load и тому подобное, тысячи их. В Си++ можно использовать std::atomic.
Happy lockless coding!