Многозадачность, мутексы и lockless доступ

Представим себе скетч, многозадачный, в котором мы оперируем с какой-нибудь структурой, назовем ее

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 инициализирован где-то), но есть несколько проблем:

  1. На мутексе задача ,блокируется и ничего не делает, пока мутекс не разблокируется. Создается задержка.
  2. С мутексами существует проблема нарваться на deadlock.
  3. Если удалить задачу, которая захватила и не отпустила мутекс, то всё, привет.
  4. А еще - мутекс надо создавать и удалять. Ну, или хотя-бы создавать, бог с ним , с удалением.

Слава богу, на этих наших ардуинах многозадачных построенных на 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!

2 лайка

Эт ты счас открыл Critical Section

Не, critical section еще и прерывания выключает, т.е. это тяжеловесненько. А во вторых, в critical section, обычно, вот такой код

while (variable > 0) {}

Т.е. крутится на месте, блокируя все, вообще все.

Я же написал про безблокировочный подход

Интересненько… Вообще, в С и С++ “есть многое, что и не снилось…”
Разве библиотека std.atomic доступна “в этих наших ардуинах” ? Для конкретики - скажем на обычной АВР Атмега328 ?

Справедливости ради - на них и истинная многозадачность-то излишняя ))

Кто ж его знает. Надо компилятор изучать. На ESP32 - работает :).