C++ Embedded
424 subscribers
2 photos
16 videos
3 files
14 links
Леденящие душу прохладные истории про С++ в embedded проектах. Зарисовки из разработки встраиваемых систем.
Download Telegram
std::type_identity
"Что это?" - маститый разработчик недоуменно таращился на мой код, - "решил создать самую бесполезную метафункцию в плюсовой истории?"
Тут же заливистый смех наполнил окрестности.
template<typename _Tp>
struct type_identity { using type = _Tp; };

"Зачем тебе получать значение типа, если он тебе известен заранее?"
static_assert(std::is_same_v<std::type_identity_t<int>, int>);

"Зачем оно тебе надо?" - не унимался наш местный маэстро.
"В стандарт пристрою!" - дерзко отвечаю я, устав слушать клокочущее горловое бульканье, - "я и пропозл написал!"
Последняя реплика повергает Маститого на пол, и всхлипы доносятся уже из-под стола.
"Ладно, написал не я, а Timur Doumler в 2018 году. Документ номер P0887R1..."
Смех резко стих. Из-под стола появилось красное и серьезное лицо: "Если Timur Doumler считает, что в этой бесполезности есть смысл, надо задуматься. Возможно, перед нами не кусок ерунды, а мощный инструмент метапрограммирования!"
В качестве причины, побудившей создать type_identity, уважаемый автор указывает необходимость в некоторых случаях отключить автоматический вывод типов (type deduction).
Тогда пользователь вашего кода, хочет он того или нет, будет указывать тип явно.
Например, чтоб избежать неоднозначности и заставить пользователя функции foo указывать шаблонный тип, то достаточно написать
template <class T>
void foo(std::type_identity_t<T>);

Если вызвать функцию без указания типа:
foo(1); 

Нарвемся на ошибку: no matching function for call to 'foo(int)'
Укажем тип явно и все соберется без проблем:
foo<int>(1);

Собственно, идея не нова. В узких кругах этот прием практикуют широко. Его даже можно назвать целой идиомой идентификации типа (type identity idiom).
Где это может пригодиться? С тех пор как в c++17 стало возможно выводить шаблонный тип прямо из конструктора, опасность не покидает наш код.
struct smart_pointer {
smart_pointer(T *) ...
};

Такой конструктор не отличит указателя на объект от указателя на массив. Неизвестно, что за тип ему вздумается вывести. Лучше сразу придушить все вольности:
template <class T>
struct smart_pointer {
smart_pointer(std::type_identity_t<T> *) ...
};

Забавно, что в одном случае type_identity требует строгости, а в другом - сделает компилятор более сговорчивым.
Например, мы хотим сложить два значения
template <class T>
T sum(T a, T b) { return a + b; }

Когда типы аргументов разные возникает небольшое недопонимание:
sum(0.F, 0);

Здесь мы хотим сложить значение float и int, ничего криминального, но: no matching function for call to sum(float, int)
Однако переписав функцию как:
template <class T>
void sum(T, std::type_identity_t<T>);

компилятор не видит тождественности типов, и если второй тип приводится к первому, то это успех.
sum(0.F, 0);  // OK

Неприводимые типы все еще будут вызывать вопросы:
struct Empty {} empty;
sum(0.F, empty); // ERROR
sum(0.F, ""); // ERROR

Еще type_identity можно использовать как базовый блок метапрограммирования.
Например, можно реализовать remove_const так:
template <class T>
struct my_remove_const : std::type_identity<T> {};
template <class T>
struct my_remove_const<T const> : std::type_identity<T> {};

Никаких лишних using внутри, красота! Так и вижу, как все бросились переписывать свою личную библиотеку шаблонов...
👍6🤔1
std::identity
Интересные своей кажущейся бесполезностью функции есть не только в compile-time. Runtime тоже может порадовать нас функтором std::identity. На cppreference можно найти пометку, что это чудо возникло в C++20, но это не совсем так. Давным-давно SGI (Silicon Graphics, Inc) уже предоставлял identity. Если уж без функтора не обойтись, но делать ничего не надо, то identity может быть использован как холостой. Его operator() возвращает переданный аргумент без изменений.
По рассказам современников, выглядел он примерно так:
template <class T>
struct identity : public unary_function<T, T> {
T& operator()(T& x) const { return x; }
const T& operator()(const T& x) const { return x; }
};

Жила себе эта колхозная реализация где-то в functional и в ус не дула.
Однако беспокойные умы, дабы улучшить плюсы, решили ввести известную нам метафункцию в стандарт:
template <class T>
struct identity { typedef T type; };

Ну, так получилось, что именно в таком виде положили ее в utility.
Неприятности начались практически сразу же. Уже через два года (в ноябре 2007) появилась первая - проблема №700.
Документ N1856 от 26 августа 2005 года предполагает создание в заголовочном файле utility структуру identity, которая конфликтует с уже существующей нестандартной реализацией identity в functional.

А что мы делаем, если видим два класса с одинаковыми названиями? Конечно! Их нужно объединить для обратной совместимости и всего делов:
template <class T>
struct identity {
typedef T type;
const T& operator()(const T& x) const;
};

Очень странно, что после такого блестящего решения обнаружились новые изъяны. Проблема №823 зафиксирована в марте 2008 года.
В документе N2588 предлагается объединить два определения, но это ломает identity<void>.

Теперь стало невозможно определить identity<void>, поскольку для этого нам понадобится ссылочный тип void, что неестественно и отвратительно.
Решить можно и эту проблему, нужно просто ввести частичную специализацию для identity<void>.
Наконец, в декабре 2008 года подоспела проблема №939, где критиковалась работа std::identity со ссылками на временные объекты.
std::identity берет аргумент T const & на входе и выдает такой же на выходе. К сожалению, это позволяет передать объект отличного от типа T, но конвертируемого в него, и вернуть ссылку на уничтоженный временный объект.

Вариантов исправлений было много. Выбран был самый оптимальный: удалить к черту, вон из стандарта! Тем более что даже std::forward больше не опирался на identity.
Так бесславно закончилось первая попытка стандартизации.
Второе пришествие std::identity состоялось уже после 2018 года, с появлением документа P0898 Standard Library Concepts.
Там тихо-мирно вводится определение дополнительного класса identity, в стандарте теперь можно найти по адресу 22.10.12 Class identity [func.identity]
Реализован в ranges_cmp.h и очень похож на неповторимый оригинал от SGI, только используется perfect forwarding для передачи аргумента.
struct identity {
template<typename _Tp>
[[nodiscard]]
constexpr _Tp&& operator()(_Tp&& __t) const noexcept {
return std::forward<_Tp>(__t);
}
using is_transparent = __is_transparent;
};

Какого рожна ranges потребовались эта бесполезная функция? Использовать этот функтор напрямую в общем-то бессмысленно. Но std::identity служит как отображение значений по умолчанию.
Да, есть такая фишка в пространстве имен ranges. Для множества алгоритмов можно указать функтор projection - вызываемый объект, применяемый к каждому элементу диапазона перед обработкой. Например:
std::vector<int> w {{1, 2, 3}};
auto projection = [](int x) { return x + 10; };
auto result = std::ranges::all_of(w, [](int x){ return x < 10; }, projection); // result == false

Здесь каждому значению будет прибавлено 10 перед сравнением. Если мы не укажем ничего вместо параметра projection, то все значения будут сравниваться с 10 напрямую.
Кстати, старую версию от SGI тоже можно найти в GCC (ext/functional). Оставили в назидание потомкам.
👍31🤔1
std::is_bounded_array or std::is_unbounded_array
Легко писать про безобразные собеседования. Чем хуже оно проходит, тем лучше. Совсем превосходно, если интервью заканчивается грязной дракой. О, сколько контента можно навалить опосля! Поэтому складывается впечатление, что индустрия на грани, специалистов не хватает, и все очень-очень плохо. Однако это не совсем так, и приятные исключения все же бывают. Вот третьего дня пришли ко мне люди из молодой и подающей надежды компании А-стор, схватили и понесли на собеседование.
Там суровый мужчина не задавал странных вопросов. Все они так или иначе касались предметной области.
"Вот есть такая функция lseek... может ли он шагнуть за границу файла?" - интервьюер с надеждой посмотрел на меня.
"Дааа...", - говорю, а сам пытаюсь вспомнить хоть что-то про эту функцию, - "границы вещь такая..."
Вот Уильям Пенн Эдер "Уилл" Роджерс говорил, что когда невежество начинается, то границ оно не знает. Кстати, это эпиграф к предложению "Traits for [Un]bounded Arrays" от 2019 года. Автор предлагал ввести в язык новые метафункции: is_bounded_array и is_unbounded_array.
Это очень просто, ведь массив можно определить как T[N] - то есть внутри скобок указан размер N, это bounded array.
Либо задать массив как T[], это unbounded array.
Думаю, опытные разработчики только фыркнут, мол, такое сделать несложно:
template<typename T>
inline constexpr bool is_bounded_array_v = false;
template<typename T, size_t Size>
inline constexpr bool is_bounded_array_v<T[Size]> = true;

Если тип T совпадает с шаблоном T[Size], то возвращаем true.
Аналогично реализуется и функция для беспредельного массива.
Хотя GCC может задействовать и встроенные функции __is_unbounded_array и __is_bounded_array.
Вся эта веселая возня наводит на мысль, что тип массива - это нечто большее, нежели указатель. Убедимся в этом, откроем Itanium C++ ABI.
В описании декорирования имен смотрим пунк 5.1.5.6 Array types. Типы массивов кодируются их размером и типом элементов. Заметьте, "массивные" аргументы функции кодируются как указатели. Размер массива опускается для неполных типов массивов.
Схема кодирования:
<array-type> ::= A [<array bound number>] _ <element type>
::= A <instantiation-dependent array bound expression> _ <element type>

То есть int[6] будет декорирован как A6_i. Тип сохраняется, и это очень важно для метапрограммирования.
int x[10] {};
template <class T>
void func(T t) {...} // T = int*

При вызове func(x), конечно же, никто в здравом уме не станет копировать массив по значению, как и велит ABI, массив передается по указателю. Однако, если мы используем universal reference, то тип передается точнее
template <class T>
void func2(T &&t) {...} // T = int (&)[10]

Можно и механизм перегрузки задействовать, тоже будет работать.
int sum(int (&arr) [10]) -> _Z3sumRA6_i
int sum(int (&arr) []) -> _Z3sumRA_i

Всегда полезно знать полную информацию о массиве, особенно когда жонглируешь шаблонами. Например, если где-то мы конструируем объект на основе переданного типа, то нужно зорко следить, чтоб не подсунули беспредельный массив.
"Все это очень интересно, но только никак не относится к сути вопроса", - собеседник был явно ошарашен.
Короче говоря, не самое мое удачное интервью. Хотя бы оплеванным после него себя не чувствуешь. Через пару дней, к моему удивлению, мне все же сделали интересное предложение. Без шума и пыли. Респект.
👍42🤣1
thread_local
Я просыпаюсь в холодном поту. Резко вскинувшись от ужаса, остаюсь сидеть в кровати. Снова снился FreeRTOS. Вырвавшиеся на свободу таски с остервенением рвали незащищенные структуры данных. "Почему ты не защитил нас!.." - доносился до меня их предсмертный хрип. Я пометил их как thread_local, но это не работало. Безжалостные и неумолимые жернова многопоточности растерзали и перемололи хрупкие участки памяти. Почему же такая защита не помогла?
Думаю, все знают, что такое thread_local. Переменная с такой меткой в каждом потоке имеет свое собственное значение. Яркой иллюстрацией может послужить переменная errno. Устанавливая ее в отдельном потоке, мы не хотим, чтоб ее значение было видно в другом.
Стандарт опять же, в разделе 6.7.5.3 Thread storage duration [basic.stc.thread], гарантирует нам, что:
Переменные, объявленные с ключевым словом thread_local, хранятся, пока существует специальное потоковое вместилище. Мамой клянусь, оно будет существовать, пока выполняется поток.
Каждому потоку достанется отдельный объект или ссылка на него. Объявленное имя будет связано с сущностью текущего потока.
Из этого воспоследует, что неплохо бы озаботиться неким специальным участком памяти для хранения такого типа переменных. Он есть, и имя ему TLS (thread local storage). Как это будет реализовано, отдается на откуп компилятору. Есть специальная опция для сборки GCC, которая выбирает метод доступа к локальному хранилищу потока -mtls-dialect=gnu. Посмотрим, какой же метод выбран у нас. Наш-то arm-none-eabi-g++ собран с --disable-tls, т.е. целевая платформа вообще не поддерживает TLS. Логично, у нас тут настоящий embedded, а не гулькин клюв. Эксплуатируемый нами GCC жесткий, собран без поддержки потоков. Зачем TLS?
Да, что-то вроде потоков есть во FreeRTOS, но без поддержки GCC thread_local ничего не стоит. Конечно, компилятор не посмеет отказать нам в использовании ключевого слова.
Код соберется, GCC не будет грязно ругаться. Дело в том, что даже собранный с --disable-tls компилятор хоть чучелком, хоть тушкой, но обязан поддержать спецификатор. В этом случае управление TLS переходит с т.н. emulated TLS, то есть эмуляции потоковых хранилищ.
Прослойка эмуляции создает объект управления для целевого объекта. Для доступа к целевому объекту TLS имеется функция поиска, которая при указании адреса объекта управления вернет адрес экземпляра целевого объекта для текущего потока.
Рассмотрим, как это работает на практике. Для пущего веселья определим некую структуру Info:
 
struct Info {
int x;
int y;
};

Теперь напишем якобы thread-safe функцию, выполняющую бессмысленные действие:
 
int safe_func(int t) {
thread_local Info common {};
common.x += t;
common.y += t;
return common.x + common.y;
}

Сначала может показаться, что common - это локальная переменная. Гоните от себя эту мысль! Стандарт недвусмысленно намекает в разделе 9.2.2 Storage class specifiers [dcl.stc]:
Если переменная помечена как thread_local, то она по умолчанию интерпретируется как static, если иной спецификатор не объявлен явно.
На математическую составляющую можно смело закрыть глаза, нас интересует только доступ к целевому объекту - переменной common. Вот как это выглядит в ассемблерном кружеве:
080005f0 <safe_func(int)>:
80005f0: push {r7, lr}
80005f2: sub sp, #8
80005f4: add r7, sp, #0
80005f6: str r0, [r7, #4]
80005f8: ldr r0, [pc, #36]
80005fa: bl 800053c <__emutls_get_address>
80005fe: mov r3, r0

Доступ к объекту выдает функция __emutls_get_address, это и есть упомянутая выше функция поиска.
Сигнатура функции: void *__emutls_get_address (struct __emutls_object *obj), где obj - указатель на управляющий объект.
И тут нас всех осеняет: "А можно ли ее подменить?" A suivre.
👍4💯1
single-thread & __emutls_get_address
"Мечта у меня имеется. Хочу под FreeRTOS на плюсах писать. Чтоб там std::thread были и всякое..." - неосторожно вырвалось у меня во время аттестации и вместо повышения грейда вышло увольнение. Однако это не должно нас останавливать!
Итак, мы решили подменить функцию поиска __emutls_get_address. Входным аргументом функции будет указатель на объект управления.
Что это за объект? Обратимся за разъяснениями к GCC. В его исходном коде можно найти некую подозрительную структуру __emutls_object.
Выглядит она следующим образом:
struct __emutls_object {
word size;
word align;
union {
pointer offset;
void *ptr;
} loc;
void *templ;
};

Интуитивно понятно, что такое word и pointer:
typedef unsigned int word __attribute__((mode(word)));
typedef unsigned int pointer __attribute__((mode(pointer)));

Тип целочисленный, но вот ширина определяется через атрибут mode. Режим word определяет ширину однословного целого числа, а pointer задаст такую же ширину, как и у указателя.
Теперь рассмотрим, из чего состоит управляющий объект:
size - это, конечно, размер целевого класса. Столько памяти мы должны выделить под объект в текущем потоке.
align - величина выравнивания целевого класса.
В функцию поиска мы ворвемся с уже установленными параметрами. Остальные члены структуры вообще непонятно для чего, вернемся к ним позже.
Когда же вызывается функция поиска? Вспомним наш пример:
int safe_func(int t) {
thread_local Info common {};
common.x += t;
...

Специально для safe_func компилятор создаст в где-то памяти объект типа __emutls_object, и когда мы заходим обратиться к common, он "найдет" нужный экземпляр объекта через __emutls_get_address, передав ей указатель на управляющий объект. Функция вернет указатель на найденный целевой объект (актуальный для текущего потока).
Сомневаюсь, что здесь собрались любители мучить велосипеды, поэтому одним глазком взглянем, как GCC реализует функцию поиска.
Сначала рассмотрим вариант для компилятора, собранного без поддержки потоков, все основные события развернутся в первой части функции:
void * __emutls_get_address (struct __emutls_object *obj) {
if (! __gthread_active_p ()) {
if (obj->loc.ptr == NULL)) obj->loc.ptr = emutls_alloc (obj);
return obj->loc.ptr;
}
...

Здесь функция __gthread_active_p проверяет на всякий случай, активна ли система потоков, если да, то возвращает 1, иначе 0.
Поскольку потоки не поддерживаются, то мы попадаем внутрь if и проверяем, выделена ли уже память для целевого объекта. Если нет, то запрашиваем память через emutls_alloc.
Потоков нет, поэтому одного участка памяти достаточно, его можно записать в obj->loc.ptr и впоследствии всегда использовать этот указатель.
Почему нужна специальная функция для выделения памяти? Почему не malloc? Как вы помните, управляющий объект содержит не только размер, но и величину выравнивания. Т.е. память нужно выделить уже выровненную, чего malloc делать не умеет.
Еще один момент, emutls_alloc инициализирует участок памяти. Выглядит это так:
if (obj->templ)
memcpy (ret, obj->templ, obj->size);
else
memset (ret, 0, obj->size);

Вот нам и пригодился последний неопознанный параметр obj->templ. Если он указывает на какой-то паттерн, то им целевой объект и инициализируется.
Предельно простое поведение при однопоточной модели компилятора. Теперь понятно, почему thread_local переменные абсолютно "сломаны" во FreeRTOS.
Зато мы видим, как это можно исправить, думаю, управимся до праздников. A suivre.
🔥3😎1
FreeRTOS & __emutls_get_address
Безработным быть не так уж и плохо. Не нужно больше вставать чуть свет, можно поваляться до шести утра. Не спеша встать, приготовить горький кофе, поплакать немного под песню "Я хочу быть кочегаром" и продолжить свои дьявольские эксперименты по интенсивному скрещиванию плюсов с FreeRTOS. Удивительно, но в эту ОСВР завезли аналогичную thread_local функциональность. Без поддержки в языке, конечно, но вызовом пары свободных функций можно добиться желаемого эффекта.
void vTaskSetThreadLocalStoragePointer(TaskHandle_t xTaskToSet, BaseType_t xIndex, void *pvValue)

Устанавливает некий указатель pvValue по некоему глобальному индексу xIndex для некоего потока (или задачи) xTaskToSet. Если же xTaskToSet равен NULL, то переданный указатель будет сохранен для текущего потока. Можно воспринимать это как двухмерный массив указателей, где индексами служат значения xIndex и дескриптор потока.
Чтоб получить это значение, нам понадобится функция:
void *pvTaskGetThreadLocalStoragePointer(TaskHandle_t xTaskToQuery, BaseType_t xIndex)

Тут ожидаемо фигурирует глобальный индекс xIndex. Опять же xTaskToQuery можно не указывать, тогда вернется актуальное значение указателя для текущего потока.
На самом деле каждый поток резервирует массив указателей в себе, размер массива определяется дефайном configNUM_THREAD_LOCAL_STORAGE_POINTERS.
С такими функциями не нужно ничего выдумывать, стоит просто начать реализовывать.
Попробуем переопределить многократно упомянутую ранее функцию поиска. Просто вписать void* __emutls_get_address(__emutls_object*) не получится:
error: definition of 'void* __emutls_get_address(__emutls_object*)' ambiguates built-in declaration 'void* __emutls_get_address(void*)'

Придется использовать такую сигнатуру, как у встроенной функции, ибо кое-кто не хочет светить свои внутренние структуры наружу. Понимаем.
void *__emutls_get_address (void *o);

уже внутри без помех приведем указатель к нужному типу:
struct __emutls_object * obj = static_cast<struct __emutls_object *>(o);

Получается, что каждый новый объект управления должен получить свой уникальный номер, индекс в массиве. Используем сквозную нумерацию, введем счетчик:
static int __emutls_index {};

будем увеличивать его каждый раз, когда функция поиска вызывается для нового управляющего объекта. Хранить полученный ассоциированный индекс можно хоть в переменной obj->loc.offset. Условимся, что нулевое значение указывает на отсутствие назначенного индекса.
if (obj->loc.offset == 0) {
obj->loc.offset = ++__emutls_index;
}

Далее попробуем получить наш указатель через pvTaskGetThreadLocalStoragePointer(NULL, index).
Если фиаско мы потерпели, значит, память под целевой объект еще даже не выделена. Тут же исправим ситуацию, выделив выровненную память (через emutls_alloc, например) и сохранив указатель на нее в vTaskSetThreadLocalStoragePointer.
И все, и все, и все... сохранять в управляющем объекте этот указатель не имеет смысла, просто вернем его.
Итого, самая простая реализация thread_local для FreeRTOS будет выглядеть так:
extern "C" void *__emutls_get_address (void *o) {
struct __emutls_object * obj = static_cast<struct __emutls_object *>(o);
if (obj->loc.offset == 0) {
obj->loc.offset = ++__emutls_index;
}
int const index {static_cast<int32_t>(obj->loc.offset) - 1};
void *ptr = pvTaskGetThreadLocalStoragePointer(NULL, index);
if (ptr == nullptr) {
ptr = emutls_alloc(obj);
vTaskSetThreadLocalStoragePointer(NULL, index, ptr);
}
return ptr;
}

Довольно примитивно, зато останется еще время нарезать новогодний салат. Хотя после увольнения со временем у меня теперь проблем нет. Еще нормализовалось давление, появился здоровый румянец на лице и иных местах. Лохматость повысилась... того и гляди, почки придут в нормативное состояние, дороже стоить будут.
Тем ихтиандрам, кто не выдержал и отписался, традиционно, попутного ветра и успешных собесов в ООО "Геенна Огненная".
Остальных, сильным духом и крепких умом, поздравляю с наступающими праздниками! С Новым кодом!
🎄12🤝1
never trust a voise on the phone
Звонок. "Вас приветствует компания "Машинно-тракторная станция"! К сожалению, заканчивается срок действия вашего договора..." - стал зачитывать скрипт противный гнусавый голос.
Всегда отношусь с подозрением к звонкам с неизвестного номера, но здесь жир буквально сочился через трубку. Во-первых, эта схема развода уже сто лет как известна. Во-вторых, от красной компании я в ужасе бежал еще три тысячи лет назад, прихватив на прощание номер. Об этом не знают только всякие жухалы, что и выдает их с головой. Не говоря уже о предложениях, по степени нелепости сравнимых только с просьбой пускать ветра на зажигалку в общественном месте.
"Договор просто необходимо продлить, - с жаром подтверждаю я, подавив первоначальный импульс немедля закончить разговор, - непременнейшие!"
"Хорошо, сейчас придет смс..."
"Да, да, да!.. - почти кричу я, - уже пришла, там написано, цитирую: катись колбаской по Малой Спасской! Ага, и код есть: 53"
"Почему 53?" - неуверенно осведомился гнусавый.
"Да потому, что ты отброс!"
Это шутка для своих. По-японски число 53 можно прочитать как "го-ми", что в переводе значит просто "мусор".
Дальше я не стал давить языковыми познаниями, а просто выдал псевдоспециалисту службы поддержки опсоса весь вокабуляр, бережно собранный во времена работы грузчиком на атомной станции. Гнусавого не хватило надолго, и он, заплакав, повесил трубку.
Получи! На мякине не проведешь! Обычно вообще звонки не принимаю с незнакомых и подозрительных номеров, но сейчас я обновил резюме и надеялся, что позвонит какой-нибудь Цукербрин.
Вечером того же дня мои ожидания оправдались.
"Здравствуйте! Я из компании "Большая Медведица", - голос сразу показался мне приятным, хоть и был с нарушением резонаторной функции носовой полости, - видели ваше резюме, вы и правда собрались увольняться?"
"Да, думаю об этом".
"В общем, не буду ходить вокруг да около, нам все нравится, даже собеседование нет нужды проводить. Однако есть маленькая проблема"
"Какая?" - внутреннее ликование сменилось тревогой.
"Вы же работаете в компании "Малая Медведица", а это наш подрядчик. Мы не можем вас переманивать и даже обсуждать наше предложение..."
У нас и правда были проекты с этим заказчиком.
"Я понял, чтоб вы сделали мне предложение, мне нужно быть свободным, как ветер?"
"Вы очень догадливы. Дайте нам знать, когда напишите заявление. Не затягивайте."
На этом наш разговор закончился. После столь воодушевляющего звонка я, обдумав предложение секунд пять, написал менеджеру, что устал. Потом написал коллегам, что ухожу. Собственноручно состряпал заявление о непреодолимом желании покинуть стройные ряды компании "Малая Медведица".
Положенные две последующие недели я провел как на иголках. Таинственному благодетелю из "Большой Медведицы" о принятом решении я сообщил сразу. В ответ получил короткое: "Хорошо". Больше им не писал, чтоб не думали, будто они мне нужны больше, чем я им. Еще свои условия будут диктовать. Мол, мне податься больше некуда, других предложений нет.
Так прошла неделя. Я был тверд, как скала. Они тоже не писали. "Наверное ждут, пока уволюсь окончательно...", - не отчаивался я.
Прошло две недели. Я получил расчет, у меня отобрали всю скопившуюся коллекцию плат для разработки. Делать стало решительно нечего. На связь никто не выходил.
На исходе третьей недели, когда костлявая рука голода протянула свои холодные пальцы к моему горлу, твердость моя дала слабину и стала походить на весеннюю снежную жижу. Махнув рукой на гордость я написал сообщение, где бесстыдно умолял сделать мне оффер, они ведь хотели, хоть прямо и не говорили.
В ответ пришел лишь подмигивающий смайлик и пара цифр: 5 и 3.
👍8👏3🦄2😁1
std::thread
Очень волнительно вдруг стать безработным. Длительное пребывание в таком статусе уже не особо беспокоит. Все как-то меньше желания выделывать коленца на собеседованиях перед пресыщенной публикой. Лучше поспать, или погулять возле свалки, или... продолжить дьявольские изыскания по натягиванию плюсов на FreeRTOS! Помнится, с thread_local мы более-менее разобрались. Только зачем нам оно, если у нас нет нормальных потоков? Конечно, на микроконтроллерах это никому не нужно, но можем же?!
Давайте посмотрим, достаточно ли глубока кроличья нора, чтоб сунуть туда еще и std::thread.
Взглянем, наконец, на std::thread с точки зрения не заклинателя потоков, а разработчика. Несмотря на кажущуюся сложность, структура std::thread проста: он содержит только один член класса _M_id типа id. Класс id внутренний с неким хэндлером _M_thread, типа native_handle_type.
class id {
native_handle_type _M_thread;
public:
id() noexcept : _M_thread() { }
explicit id(native_handle_type __id) : _M_thread(__id) { }
...

Что скрывается под этой маской, зависит от конкретной реализации, но именно _M_thread держит в себе внутреннюю суть потока.
Создавая объект потока, мы ожидаем мгновенного запуска.
std::thread first {func, arg1, arg2};

Т.е. вызов конструктора first тут же породит поток, в котором будет вызвана функция func с аргументами arg1 и arg2.
Закрадывается подозрение, что конструктор потока лишь удобная обертка для вызова системных функций.
Вот как выглядит конструктор std::thread, если убрать малозначительные детали:
template<typename _Callable, typename... _Args, typename = _Require<__not_same<_Callable>>>
explicit thread(_Callable&& __f, _Args&&... __args) {
static_assert(...); // std::thread arguments must be invocable
using _Wrapper = _Call_wrapper<_Callable, _Args...>;
_M_start_thread(_State_ptr(new _State_impl<_Wrapper>(std::forward<_Callable>(__f), std::forward<_Args>(__args)...)),
nullptr);
}

Хорошо видно, что в конструктор можно пихать все, что угодно. К сожалению, конструктор ограничивает лишь SFINAE, который запрещает передавать внутрь объекты thread. Иначе это будет уже конструктором копирования. Хорошо бы еще сразу на входе отсечь невызываемые объекты, чтоб не добавлять головной боли метапрограммистам. Иначе они опять решат, что выражение std::thread{1} имеет смысл.
Однако ограничение есть только внутри конструктора в виде static_assert, что не очень хорошая практика:
static_assert( __is_invocable<typename decay<_Callable>::type, typename decay<_Args>::type...>::value)

Ведь из-за этого следующее выражение очень даже пройдет валидацию:
static_assert(std::is_constructible_v<std::thread, int>);  // Сомнительно, но ОК

А потом сборка упадет в корчах, измарав весь вывод малопонятными сообщениями об ошибках.
Наконец, спустимся еще ниже и найдем там вызов функции _M_start_thread. Именно она выполняет всю грязную работу.
К ее реализации еще вернемся, а сейчас нас интересует что же за красота такая в аргументах.
Там создает объект типа _State_ptr, он же std::unique_ptr<_State>.
Сразу понятно, что _State - базовый класс:
struct _State {
virtual ~_State();
virtual void _M_run() = 0;
};

Конструируемый наследник _State_impl содержит в себе некий функционалный объект, который может быть исполнен путем вызова _M_run.
Этот объект вызывается без аргументов, и если мы хотим в потоке вызвать функцию с аргументами, то надо еще постараться. Хорошо, что делать это за нас будет _Wrapper.
Он же _Call_wrapper, он же _Invoker, создает обертку вызова желаемой функции с некоторым количеством аргументов. Т.е. просто впитает в себя func, arg1, arg2 и будет хранить в tuple, пока не настанет час Ч. И где-то внутри _M_start_thread вызовет func(arg1, arg2), прием давно известный и малоинтересный. Хорошо, что это уже реализовано до нас.
Фуф, после праздников нельзя так резко начинать. Надо поспать или погулять... _M_start_thread никуда не убежит. A suivre.
👍61
thread::_M_start_thread
"... они много кричат об опасности усреднения в художественном переводе, но позвольте... - небритый теоретик с воскового цвета лицом картинно вскинул руку, - ... другая сторона медали, где художественный перевод есть оглупление автора и читателя..."
"Отец, ты не из иняза? - лекция мне надоела, - а то заточка твоя уж больно знакомая". Аккуратно плечом оттер увлекшегося оратора от контейнера. Много вкусного и полезного может найти там вынужденный фриган, если хорошенько пороется.
А если хорошенько покопаться в исходниках GCC, можно найти даже готовую к употреблению функцию _M_start_thread в файле thread.cc:
void thread::_M_start_thread(_State_ptr state, void (*depend)()) {
...
const int err = __gthread_create(&_M_id._M_thread, &execute_native_thread_routine, state.get());
if (err) __throw_system_error(err);
state.release();

Для тех, кто забыл, т.е. для меня, напомним, что в качестве аргументов в функцию передается умный указатель state и какой-то малопонятный depend. Второй аргумент вообще не несет никакой алгоритмической нагрузки, его пока отложим.
Вот внутреннее значение умного указателя нам еще пригодится, мы его отдадим в вызов функции __gthread_create. Также туда мы сплавим и указатель на _M_id._M_thread. Вы, конечно, помните, что у класса thread есть член _M_id типа id, содержащий в себе _M_thread - душу потока. Естественно, туда же передадим указатель на функцию execute_native_thread_routine, непосредственно она и будет запущена в отдельном потоке.
Ой, какая стыдная картина нам тут открывается, просто грязный хак: в функцию мы передаем сырой указатель, полученный через state.get().
В конце функции наш unique_ptr не уничтожается, но обнуляется вызовом release(). Что поделаешь, __gthread_create пришел из незамутненного сишного мира, где нет этих ваших срам-поинтеров.
В функции __gthread_create нет абсолютно ничего примечательного, кроме того, реализация зависит от платформы
static inline int __gthread_create (__gthread_t *__threadid, void *(*__func) (void*), void *__args) {
return __gthrw_(pthread_create) (__threadid, NULL, __func, __args);
}

Чаще всего это просто системный вызов pthread_create. Думаю, все знают какие аргументы он ждет.
И вот стартует новый поток, execute_native_thread_routine начинает исполнять.
static void* execute_native_thread_routine(void* __p) {
thread::_State_ptr __t{ static_cast<thread::_State*>(__p) };
__t->_M_run();
return nullptr;
}

Здесь все предельно просто. Воссоздаем уникальный указатель thread::_State_ptr, ну в самом деле, не в вручную же его удалять? Запускаем наконец функцию, которую мы изначально и хотели запустить в потоке. Вот и дождались, легким движением руки вызов _M_run превращается в вызов целевой функции со всеми ее сохраненными аргументами. Если мы не ставили себе целью запустить бесконечный поток, а лишь использовать его временно, то рано или поздно из функции мы выйдем, _M_run завершится функция вернет nullptr, умный указатель освободит сохраненную функцию с ее многострадальными аргументами. И все!
Какой второй аргумент? А, depend! Ну, он тут просто так, участвует только в одной операции внутри функции:
asm ("" : : "rm" (depend));

Штука это не новая, нечто подобное использовал для реализации своего std::launder. Здесь так же, ассемблерная вставка нужна для предотвращения оптимизации, даже LTO побрезгует оптимизировать такой кошмар. Обычно в качестве второго аргумента _M_start_thread выступает функция _M_thread_deps_never_run.
static void _M_thread_deps_never_run() {
reinterpret_cast<void (*)(void)>(&pthread_create)();
reinterpret_cast<void (*)(void)>(&pthread_join)();
}

Очередной грязынй трюк. Чтоб не полагаться на вызов функций gthread, мы засветим нужные функции заранее в аргументе, который гарантированно не будет оптимизирован. Сильные ссылки на функции pthread_* точно будут.
Теперь, когда все с этими потоками стало ясно, самое время реализовать их для FreeRTOS. A suivre.
👍41
std::thread & FreeRTOS
Нихао всем, кто еще не выкинул елку. Верной дорогой идете, товарищи! Сейчас же еще китайский новый год праздновать. В этот день принято шуметь, чтоб отогнать злых духов. Мы же сделаем им больно иным путем: используем таски FreeRTOS для создания потоков а-ля С++. Поскольку GCC собран без поддержки многопоточности, никаких стандартных потоков у нас нет, про FreeRTOS компилятор тоже не имеет никакого представления. Если все же подключить заголовочный файл <thread> и создать нечто такое: std::thread first{}; то все соберется, вот только объект first будет абсолютно бесполезен. Никакого потока мы не запустим. Заглянув внутрь, сразу увидим где рылся крымский хан:
class thread {
public:
#ifdef _GLIBCXX_HAS_GTHREADS
...

Все самое интересное внутри класса вырезано дефайном _GLIBCXX_HAS_GTHREADS, который не определен, если компилятор беспоточный. В принципе логично, но можно ли одурачить компилятор и насильно определить этот дефайн?
Добавим -D_GLIBCXX_HAS_GTHREADS в Tool settings проекта и... ужаснемся количеству ошибок при сборке.
Стандартную библиотеку не пересоберешь, а вновь открывшиеся участки кода требуют сатисфакции: функции работы с мьютексами, условными переменными и т.п. оказались без определения. Похоже, придется переписать их все для FreeRTOS. Но... мы и так собирались это делать. Пока просто застабим все, на что компилятор грязно ругается.
Нас интересуют не шпили, но функции без реализации из thread.cc вроде thread::join() или thread::detach().
В первую очередь нам нужно аккуратно реализовать _M_start_thread! Для этого нам нужно определить тип thread::id::native_handle_type.
Поскольку теперь native_handle_type просто псевдоним __gthread_t, то напишем:
typedef struct gthread {
TaskHandle_t taskID;
friend bool operator==(const gthread& a, const gthread& b);
constexpr std::strong_ordering operator<=>(const gthread& a) const;
} __gthread_t;

Имя типа TaskHandle_t как бы подсказывает нам, что переменная taskID играет роль потокового дескриптора в этой системе, поэтому нам нужно его сохранить. Еще нам потребуются функции сравнения потоков, дабы знать как их различать, но эти задачи тривиальны.
Реализуем _M_start_thread через вызов функции xTaskCreate, которая создает новую таску во FreeRTOS.
void thread::_M_start_thread(thread::_State_ptr state, void (*)()) {
BaseType_t result = xTaskCreate(static_cast<TaskFunction_t>(&execute_native_thread_routine),
(const portCHAR *)"c++thread", // имя таски
128, // глубина стека в словах (не байтах!)
state.get(), // параметр для вызываемой функции
tskIDLE_PRIORITY + 1, // приоритет такси
&this->_M_id._M_thread); // дексриптор потока
if (result != pdPASS) { std::abort(); }
state.release();
}

Элементарно! Здесь создаем таску FreeRTOS с именем c++thread, глубиной стека в 128 слов (мне для демонстрационных целей хватит), приоритетом чуть выше, чем Idle. Тут же мы воспользуемся приемом, что видели и в обычном запуске тредов. Передаем указатель unique_ptr как void*, а я что сделаю?! xTaskCreate принимает только функции типа TaskFunction_t, а у них в качестве аргумента только указатель на void.
Ах, да! Функция execute_native_thread_routine:
static void execute_native_thread_routine(void* __p) {
thread::_State_ptr __t{ static_cast<thread::_State*>(__p) };
__t->_M_run();
vTaskDelete(NULL);
}

Здесь мы работаем с умным указателем и запускаем целевую функции через _M_run. Однако если функция завершится, нам нужно корректно удалить текущую таску, поэтому вызываем vTaskDelete(NULL) в конце. Синь нянь куай лэ, тащемта!
👍4😱1🗿1
Run, std::thread, run!
Третий день плачу, чернил не достать, как, впрочем, и приличной четырехслойной бумаги. Эта зима научила нас, что сквотировать дачные домики в низкий сезон не лучшее решение, как и получение воды путем термической обработки снега. Понять это несложно, достаточно посмотреть, как превращается сейчас грохочущая слякоть в потоки не очень чистой жидкости. Совсем как таски во FreeRTOS под давлением нашего болезненного разума обращаются в самые настоящие std::thread! Настало время испытаний.
Предположим, мы создали обычный проект с поддержкой C++ через STM32CubeIDE для платы NUCLEO-L452RE. Другие девборды у меня все равно отобрали при увольнении. Серия L4 вообще заточена под сверхнизкое энегропотребление и не очень богата ресурсами, особенно мало оперативной памяти. Гейтс когда-то говорил, что 640 КБ должно хватить всем, а мне приходится довольствоваться 160 КБ. Однако если заработает здесь, то на более жирных платах уж точно.
Так вот, негодный Куб создает main.c файл, придется переименовать его вручную в main.cpp, чтоб собрался с плюсами как надо.
Немного изменим сгенерированную уныло-стандартную функцию main и ее окрестности:
struct Data { ... };
int main() {
HAL_Init();
...
Data data { ... };
std::thread main_thread {StartDefaultTask, &data};
...
osKernelStart();
std::abort();
}

Во-первых, создадим структуру Data, данные из которой понадобятся нам в потоке StartDefaultTask. Во-вторых, уберем стандартное начало для запуска потоков системой
osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);

Уж больно тянет от этого кода замшелой сишечкой. Заменим на вариант поинтереснее:
std::thread main_thread {StartDefaultTask, &data};

Хорошо, но здесь поток не запустится. А чего вы хотели? Шедулер еще не работает.
Стартанет он при вызове osKernelStart, и больше мы в функцию main не вернемся. Планировщик распределяет время между тасками, а main пролетит мимо тазика. Поэтому следующая за вызовом планировщика функция std::abort никогда не должна быть вызвана. Но пусть останется, на случай, если что-то пойдет не так.
Жмем на запуск отладки и сильно удивляемся, что все пошло в разнос: данные в структуре data превратились в кашу!
А все потому, что при запуске планировщика система вызывает такую функцию:
static void prvPortStartFirstTask( void ) {
__asm volatile(
" ldr r0, =0xE000ED08 \n" /* NVIC offset register */
" ldr r0, [r0] \n"
" ldr r0, [r0] \n"
" msr msp, r0 \n" /* Set the msp back to the start of the stack. */
...
" nop \n");
}

Вкратце, здесь мы загружаем адрес из регистра по адресу 0xE000ED08, а это Vector Table Offset Register. Загружаем первый адрес из таблицы векторов прерываний. Позвольте напомнить, что первым там идет указатель на начало стека. Инструкция msr возвращает в регистр msp адрес начала стека. Это значит, что все локальные переменные, созданные на стеке функции main, под угрозой. Да что там, им всем каюк! Более того, технически мы не покидаем main, поэтому деструкторы локальных объектов никогда не будут вызваны.
Щекотливая ситуация. Можно чуть исправить саму функцию prvPortStartFirstTask, добавить инструкцию sub, т.е. сдвинуть стек системы, оставив функции main маленькую щелочку для ее объектов: sub r0, r0, #0x100
Либо, что более предпочтительно для FreeRTOS, вообще не создавать локальных переменных в main. Перенесем все логику в MainThread и просто запустим этот поток через статический объект.
void MainThread() { ... }
static std::thread main_thread {MainThread};

Из этого потока мы не собираемся возвращаться, поэтому даже join не нужен. Это будет нашей новой точкой входа.
🔥4🤝1
nlohmann_json
Пусть FreeRTOS немного передохнет, пока мы кидаем камни в огород одной известной библиотеки для перекладывания json.
Намедни подрядился я в ближайшей it-деревне json-ы пасти. Дело нехитрое, с утра выгоняешь их на парсинг из локальных сокетов, которые местные демоны держат. Доишь из них данные и отправляешь пастись в сети. Инструмент, говорят, бери какой хочешь, но у нас есть только nlohmann_json.
Ладно, история сердцу знакомая. Вроде как современная библиотека для новейших плюсов, такое нам по нраву.
Достаем первый json, примерно такого содержания:
auto text = R"({ "pi": 3.141, "happy": true })";

Парсим в точности, как написано в инструкции:
nlohmann::json ex1 = nlohmann::json::parse(text);

Проверяем результат, просто бросая полученный объект в стандартный вывод:
std::cout << ex1 <<std::endl;  //  {"happy":true,"pi":3.141}

Отлично работает. Здесь меня обуял жуткий приступ перфекционизма: зря, что ли, AUTOSAR придумали? Надо сделать как положено! Решительно переписываю:
nlohmann::json const ex1 {nlohmann::json::parse(text)};

И... вся дальнейшая обработка ломается с треском вдребезги пополам. Стандартный вывод показывает:
[{"happy":true,"pi":3.141}]

Хм... ранее распознанный элемент неожиданно вообразил себя массивом?
За ответами лезем под капот библиотеки, и узнаем, что json - это на самом деле basic_json<>;
Далее, к ужасу своему, обнаруживаем std::initializer_list в одном из конструкторов!
basic_json(initializer_list_t init,
bool type_deduction = true,
value_t manual_type = value_t::array) { ... }

using initializer_list_t = std::initializer_list<detail::json_ref<basic_json>>;

Этот лист - коварный фрукт. Сами можете посмотреть в раздел стандарта 12.2.2.8 Initialization by list-initialization [over.match.list].
Если конструктор инициализируется списком параметров, либо конструктор по умолчанию отсутствует, сначала выполняется такое разрешение перегрузки, где функциями-кандидатами являются конструкторы инициализируемые списком (9.4.5).

То есть если объект создается через braced-init-list, значением в фигурных скобочках T{v}, а класс не агрегатный, то предпочтение всегда будет отдаваться конструктору с std::initializer_list.
Например:
struct MegaJson {
MegaJson() = default;
MegaJson(int) {}
MegaJson(std::initializer_list<int>) {}
};

Если мы напишем MegaJson j1 {1};
то при наличии std::initializer_list во втором конструкторе, на первый даже смотреть не станут.
Если мы не используем list-initialization MegaJson j1 (1);
то в дело пойдет первый конструктор.
В json-библиотеке дела обстоят еще веселее, чем в консерватории. Здесь std::initializer_list чугунной жопой заслоняет аж целый конструктор копий, поскольку в списке используются ссылки на сам класс basic_json. Для наглядности вернемся к упрощенной структуре MegaJson:
struct MegaJson {
MegaJson() = default;
MegaJson(MegaJson const &) {}
MegaJson(std::initializer_list<std::reference_wrapper<MegaJson>>) {}
};
MegaJson j1 {};
MegaJson j2 {j1};

вместо копирующего конструктора будет вызван другой. Все по стандарту, не подкопаешься.
Тогда скрипя зубами и скрепя сердце, я написал
auto const ex2 = nlohmann::json::parse(text);

Однако описанное выше поведение контринтуитивно, и вообще это безобразие. Нильсу следовало бы использовать здесь variadic templates и не выпендриваться.
👍5
std::thread memory corruption
Вот и пришло время обрушиться на слабосильный микроконтроллер всей тяжестью наших самописных потоков!
Точка входа будет в функции MainThread. Бросим туда щепотку потоков:
void MainThread() {
Data data {};
std::mutex mtx {};
std::stop_source stop{std::nostopstate};

std::thread first {startStdTask, stop.get_token(), std::ref(mtx), std::ref(data)};
std::thread second {secondThread, stop.get_token(), std::ref(mtx), std::ref(data)};

while ( 1 ) { osDelay(100); }
}

Предположим, нужно запустить два потока, работающие с общими данными data и синхронизированные mtx, при этом есть stop, выдающий потоку запрос на остановку. После запуска бесконечно сидим в главном потоке, поскольку ждать завершения мы пока не умеем.
В предвкушении жму кнопку отладки...
...и постигаю чувство полнейшего разочарования: задачи не переключаются, планировщик явно приболел. Даю последний золотой зуб соседа, память повредилась! Подозрение пало на реализацию std::thread, поскольку там есть явное динамическое выделение памяти:
_M_start_thread(_State_ptr(new _State_impl<_Wrapper>(std::forward<_Callable>(__f), std::forward<_Args>(__args)...)), __depend);

Какой бесстыдный вызов оператора new. Хоть бы аллокатором прикрылся.
Проверим, могло ли это вывести из строя систему. Нырнем поглубже и взглянем, как работает распределение памяти во FreeRTOS и его окрестностях.
У нас много памяти, аж два раздела RAM:
RAM              0x20000000:0x00028000
RAM2 0x10000000:0x00008000

Система, по сути, использует только первый диапазон 0x20000000:0x20028000.
По адресу 0x20000000 ютится небольшая секция .data в 120 байт размером. Ниже вольготно расположилась секция .bss исполинского размера: 20344 байта! Зададимся вопросом, достойным хлесткой пощечины, почему ее так разнесло? Львиную долю памяти занял некий ucHeap. Однако под личиной скромного массива скрывается зарезервированная область для управления памятью FreeRTOS.
Здесь, в общей куче, выделяется место и под стек тасков, и под TCB (Task Control Block), и даже под память, распределенную через pvPortMalloc().
В лучших традициях размер кучи задается дефайном
#define configTOTAL_HEAP_SIZE                    ((size_t)16384)

Замыкает парад секция ._user_heap_stack. Сложно сказать, какого она на самом деле размера, поскольку граничит с terra incognita, ничейной памятью.
FreeRTOS этой областью уже не владеет. Распоряжаются ей вызовы malloc/free. Динамическое выделение памяти начнется сверху, с адреса 0x20004ff0 в моем случае, и пойдет по наклонной в сторону увеличения адреса. Общий стек будет подниматься с самых низов, с адреса 0x20028000, мы видели его в регистре SP при старте.
Получается, что области динамического распределения памяти не пересекаются и портить друг другу данные не должны.
В этот момент я вспомнил слова бывшего беззаботного коллеги, ныне безработного, Олафа: "Если происходит какая-то дичь в прошивке, я просто увеличиваю размер стека".
Вообще-то это имеет смысл, локальных переменных у нас много. Стек небольшой, всего 512 байт. Мониторим память, действительно, вызовы функций пробивают стек. Граница по адресу 0x20000c30 пройдена очередным вызовом функции и затерта важная переменная uxSchedulerSuspended по адресу 0x20000c24, из-за чего планировщик встает колом.
Для контроля в системе есть функция uxTaskGetStackHighWaterMark, но метод не слишком надежный. Стек изначально заполняется паттерном 0xA5, потом в процессе жизнедеятельности потока ячейки перетираются данными, а функция подсчитывает сколько слов в памяти не подверглись модификации. Если таких ячеек в стеке осталось мало, нет гарантии, что какая-нибудь функция не перескочит их noinit данными. Тогда uxTaskGetStackHighWaterMark хоть и будет возвращать некое небольшое число, но стек будет нарушен. Кто-то писал, что возвращаемое значение меньше 50 слов - уже тревожный показатель, и тут сложно не согласиться.
Увеличим стек в несколько раз и возрадуемся бегущим потокам. Только как их теперь остановить?
👍422
std::thread is well when ends well
Давным-давно одна высокоразвитая цивилизация задумала повернуть сибирские реки вспять. Вот где был размах, вот где сила ума! Доведем же дело наших славных предков до конца. В некотором смысле. Пусть нам и не под силу развернуть реки, но хотя бы научимся останавливать потоки. И не важно, что FreeRTOS считает это отступничеством и ересью.
Если задача не зациклена, то рано или поздно ее тушка промчится мимо нас вверх по течению реки, задорно помахивая оператором возврата. Важно не упустить этот момент и выловить озорницу функцией join. Как вы помните, этот метод класса std::thread блокирует вызывающий поток до тех пор, пока текущий не закончит работу.
Сложность в том, что сейчас такой функции у нас нет. Для ее реализации нужно как-то научиться определять завершение задачи во FreeRTOS.
Представьте, что мы медленно-медленно выходим из функции-задачи... и тут же попадаем в ловушку assert-а внутри prvTaskExitError. Вообще-то, мы не должны были туда заходить, это индикатор того, что задача исполняет странное.
Что же, не зря включали опцию INCLUDE_vTaskDelay (как дефайн в FreeRTOSConfig.h) и прописывали удаление таски после выхода из целевой функции.
static void execute_native_thread_routine(void* __p) {
thread::_State_ptr __t{ static_cast<thread::_State*>(__p) };
__t->_M_run();
vTaskDelete(NULL); // Важно удалить текущую таску!
}

Тут задача не превращается в зомби, но функция vTaskDelete удаляет ее из списка живых: готовых\заблокированных\приторможенных задач и торжественно включает в проскрипционный. Специальная Idle-таска позаботится о том, чтоб вернуть выделенные на задачу ресурсы, прежде чем она исчезнет навсегда.
Включим еще опцию INCLUDE_eTaskGetState, тогда нам будем доступна еще и функция eTaskGetState, которая выдает текущий статус таски.
Для задачи, найденной в списке на удаление, конечно, вернется статус eDeleted. Однако такой же статус вернется и для объекта с полностью уничтоженным TCB. Дескриптора нет в списках? Ну, задача точно удалена.
Дело в шляпе, нам нужно только узнать дескриптор задачи. Хорошо, что он предусмотрительно записан в переменной класса _M_thread: _M_id._M_thread.taskID.
Набросаем простую версию функции join:
void thread::join() {
if (_M_id != id{}) {
eTaskState state {};
while((state = eTaskGetState(_M_id._M_thread.taskID)) != eDeleted) {
switch(state) {
case eInvalid:
__throw_system_error(eInvalid);
break;
default:
taskYIELD();
}
}
this->_M_id = id{};
}
}

Вначале проверим, возможно, мы уже обнаружили завершение задачи ранее, если _M_id пустой, то и делать ничего не надо. Если нет, то будем в цикле проверять статус задачи, до тех пор, пока он не станет eDeleted. Статус eInvalid не должен вернуться, но если вдруг получили, то у нас очень большие проблемы, бросаем исключение. В остальных случаях просто передаем управление другим потокам вызовом taskYIELD.
Осталось только ненавязчиво дать потоку знать, чтоб закруглялся, ведь мы готовы его подхватить. Раньше такое реализовывали кто во что горазд, атомарными флагами, volatile переменными, иными перверсиями. Наконец, в с++20 специально для этих целей появилось стандартизированное извращение - объект std::stop_source.
Через stop_source можно запросить остановку потока, которому был выдан связанный std::stop_token.
void firstTask(std::stop_token stop_token, std::mutex &mtx, Data &data) {
while (!stop_token.stop_requested()) {
std::lock_guard<std::mutex> lock{mtx};
osDelay(10);
}
}

Тогда в главном потоке завершение работы можно прописать так:
std::stop_source stop{};
std::thread first {firstTask, stop.get_token(), std::ref(mtx), std::ref(data)};
// делаем что-то полезное
stop.request_stop();
first.join();

Запрашиваем выход и ждем остановки. Все четко, как в маршрутном такси.
👍3🤷‍♂1
std::mutex
В выходные случайно сел в поезд и внезапно приехал в Сергиев Посад на большое совещание молодых литераторов "Посадский Экспресс". Видел, как толпы писателей и поэтов осаждают небольшое здание центральной библиотеки. Семинаров и мастер-классов было в избытке, народу еще больше! Не представляю, как организаторы обуздывали все эти бесконечные людские потоки. Свои же потоки мы будем разруливать старыми добрыми примитивами синхронизации. Мьютексом, почему нет.
Вглядимся же в глубину std::mutex:
class mutex : private __mutex_base ...
Погружаемся еще глубже:
class __mutex_base {
protected:
typedef __gthread_mutex_t __native_type;
__native_type _M_mutex;

Вот и центральный элемент базового класса _M_mutex. Только вот, что за тип такой __gthread_mutex_t?
Зависит от системы. Под FreeRTOS нужно бы самим определить. Фокус в том, что если _M_mutex можно инициализировать так
__native_type _M_mutex = __GTHREAD_MUTEX_INIT;
достаточно определить __GTHREAD_MUTEX_INIT.
Если же тип нетривиальный, то имеет смысл определить макрос __GTHREAD_MUTEX_INIT_FUNCTION.
Тогда конструктор будет чуть сложнее, но и гибче:
__mutex_base() noexcept {
__GTHREAD_MUTEX_INIT_FUNCTION(&_M_mutex);
}

Конечно, не все так просто. В нашем случае все эти "настройки" gthread компилятор уже возьмет из файла gthr-default.h, после чего переопределять их будет бесполезно. Изолируем этот файл, добавив в сборку -D_GLIBCXX_GCC_GTHR_SINGLE_H. Злоупотребим, так сказать, защитой от повторного включения.
Вместо этого файла мы навяжем компилятору другой: -include cpp_freertos\gthr-user.h
В файле gthr-user.h содержатся уже наши макросы и определения для работы с примитивами.
В файле обязательно должно быть определение типа __gthread_mutex_t:
typedef struct gthreadMutexType {
StaticSemaphore_t mutexBuffer;
SemaphoreHandle_t mutex;
} __gthread_mutex_t;

Дескриптор мьютекса во FreeRTOS имеет тип SemaphoreHandle_t, и рядом еще положим буфер типа StaticSemaphore_t, чтоб не пришлось создавать его динамически.
Теперь нужно заняться функцией инициализации
#define __GTHREAD_MUTEX_INIT_FUNCTION __gthread_mutex_init_func

Непосредственно сама функция
inline void __gthread_mutex_init_func(__gthread_mutex_t *__mutex) {
__mutex->mutex = xSemaphoreCreateMutexStatic(&__mutex->mutexBuffer);
}

Инициализация приходит через функцию xSemaphoreCreateMutexStatic, которая возвращает дескриптор мьютекса.
Для деструктора
~__mutex_base() noexcept { __gthread_mutex_destroy(&_M_mutex); }

нам просто необходимо определить еще функцию __gthread_mutex_destroy:
inline int __gthread_mutex_destroy(__gthread_mutex_t *__mutex) {
vSemaphoreDelete(__mutex->mutex);
return 0;
}

Почти готово, остались только самые важные для нормальной работы функции lock/unlock, которые прежде мы безжалостно застабили.
Функция блокировки мьютекса самая требовательная, если она вернет что-то отличное от нуля, то будет брошено исключение. Реализуем ее через вызов xSemaphoreTake
inline int __gthread_mutex_lock(__gthread_mutex_t *__mutex) {
BaseType_t const result {xSemaphoreTake(__mutex->mutex, portMAX_DELAY)};
return result == pdTRUE ? 0 : EINVAL;
}

Если INCLUDE_vTaskSuspend включен, то передача portMAX_DELAY в качестве значения второго аргумента (таймаут) приведет к тому, что задача будет заблокирована на неопределенный срок.
Для попытки блокирования нужно вызвать xSemaphoreTake с нулевым таймаутом.
inline int __gthread_mutex_trylock(__gthread_mutex_t *__mutex) {
BaseType_t const result {xSemaphoreTake(__mutex->mutex, 0)};
return static_cast<int>(result == pdTRUE);
}

Ну и __gthread_mutex_unlock будет реализован через xSemaphoreGive
inline int __gthread_mutex_unlock(__gthread_mutex_t *__mutex) {
BaseType_t const result {xSemaphoreGive(__mutex->mutex)};
return result == pdTRUE ? 0 : EINVAL;
}

Немного усилий и std::mutex готов! Однако расслабляться рано, всюду по городу шныряют поэты, а в окна заглядывают уже и другие примитивы синхронизации.
👍6🤝1
deleting destructor
- Зачем мне ваше интервью? - звонок очередного рекрутера застал меня в такси.
- Ваш пост про нас был весьма нелицеприятным, но теперь у нас все по-другому! - не сдавался голос в трубке.
- Не верю! - отрезал я и поспешно надавил на большую красную кнопку на экране смартфона.
В этот момент водитель сочувственно хмыкнул:
- Ох уж эти эйчары, спасу от них нет! Мне вот то и дело трезвонят, вся телегу загадили, а почту вообще не открываю, страшно.
Поскольку я молчал, хоть и открыл рот, таксист тут же пояснил:
- Да я на самом деле тоже разработчик, плюсовик, а таксую для души.
- Вот оно что! Коллега, значит, - вежливо улыбнулся я.
- О, тоже кресты?! Плюсики - пропуск в трусики! - собеседник коротко хохотнул, и слова хлынули из него, будто бурный поток по Ковалихинской улице в дождь, - вот третьего дня прихожу я на собес, а там дед сидит - вылитый профессор. Сначала про миссию компании нудел, аж разморило меня. Нарочно усыплял! Потом как спросит, мол, почему наследники страдают, если у базового класса деструктор невиртуальный? Коварный тип, да?
Я, погруженный в чтение бесконечного рабочего чата, рассеянно ответил, что это как раз несложный вопрос. Просто представьте, что есть нормальный базовый класс с наследником:
struct Base {
virtual ~Base() {}
virtual void get() {}
};
struct A : Base {
virtual ~A() {}
};

Тогда GCC создаст для них виртуальные таблицы vtable.
vtable для класса Base:
Base::~Base() [complete object destructor]
Base::~Base() [deleting destructor]
Base::get()
vtable для класса A:
A::~A() [complete object destructor]
A::~A() [deleting destructor]
Base::get()

Что характерно, виртуальные функции у наследника идут в таком же порядке, что и у базового класса. Это важный момент для всей этой виртуальной механики.
Допустим, сначала мы создаем экземпляр класса:
Base *a = new A;

и потом удаляем его:
delete a;

Поскольку у указателя a тип Base*, то мы находим по таблице Base::~Base() [deleting destructor] и вызываем этот деструктор. Однако на самом деле, там виртуальная таблица для класса A, и будет вызван именно деструктор A::~A() [deleting destructor].
Удаляющий же деструктор вызовет A::~A() [complete object destructor], внутри которого напрямую вызовется Base::~Base() [complete object destructor], а потом очищается память через оператор delete.
Вот если удалить виртуальный деструктор базового класса, то и таблицы кардинально поменяются.
vtable для класса Base:
Base::get()
vtable для класса A:
Base::get()
A::~A() [complete object destructor]
A::~A() [deleting destructor]

Теперь, когда мы попытаемся удалить объект типа A по указателю типа Base*, смотреть виртуальную таблицу бессмысленно - там нет деструкторов. Поэтому вызван будет без промедления Base::~Base() [complete object destructor] и очищена память. Деструктор для A остался не у дел, поэтому это плохая практика.
Таксисит слушал меня, задумчиво кивая, и, кажется, пару раз проехал на красный.
- Ладно, - рубанул он рукой воздух, - допустим, мы используем библиотеку, где автор обтриводномился и оставил базовый класс без виртуального деструктора. Что же теперь, помирать? Нам до зарезу нужна эта библиотека! Что можно сделать? Только, чур, базовый класс не менять!
- Легко! Тысячу раз так делал. - с легким зевком ответил я, а про себя подумал: "Зачем я соврал? Я ж не делал... А зачем он спросил? Зубы заговаривает. Очень подозрительный тип! Почему он свернул?! Куда мы вообще катимся?!"
🔥6👍41
deleting destructor. endgame
Я незаметно подергал ручку и убедился, что дверь заблокирована. В машине повисло неловкое молчание.
- Ну, как можно исправить ситуацию с невиртуальным базовым классом... - сказал я, чтоб только разрядить гнетущую атмосферу, - не работать с такими библиотеками, да и все!
Подозрительный шофер лишь приподнял бровь и продолжал молчать. Не перенеся такого чудовищного давления, я начал вслух размышлять.
Первое, что приходит в голову, метод грубой силы. Для нашего случая изготовим примерную структуру виртуализации объекта.
using dtor = void(*)(Base *);
struct Vtable {
dtor func[10];
};
struct BaseAlt {
Vtable *vt;
};

Альтернативный базовый класс будет содержать в себе указатель на самодельную виртуальную таблицу Vtable, а таблица пусть сплошь состоит из указателей на функции. Это должны быть указатели на виртуальные методы, но нас интересует только деструкторы, поэтому это будет массив из dtor.
Base *a = new A;
BaseAlt *brut = reinterpret_cast<BaseAlt*>(a);
brut->vt->func[2](a);

Приводим указатель a к BaseAlt* и наудачу вызываем третью функцию в псевдовиртуальной таблице. В этот раз нам повезло, и удаляющий деструктор оказался именно там. Однако гарантии никто не даст, мало ли что там компилятор накомпилировал, поэтому метод не очень хорош.
На выражение лица хмыря за рулем было страшно смотреть, будто я вскрывал банку сюрстремминга прямо в салоне. Пришлось срочно развить мысль.
Можно изготовить ложный базовый класс с предполагаемым набором виртуальных методов.
struct FakeBase {
virtual void get() {}
virtual ~FakeBase() {}
};

Поскольку реальный класс Base невиртуальный, метод get пойдет первым в классе A. Из-за этого печального обстоятельства пришлось поместить метод с такой же сигнатурой в ложном базовом классе перед деструктором. Тогда GCC выстроит правильную виртуальную таблицу, которая будет совпадать с таблицей для A. Но это неточно.
FakeBase *fb = reinterpret_cast<FakeBase*>(a);
delete fb;

Опять же, если все подогнано правильно, вызовется правильный деструктор.
Злодей, одним глазом продолжая следить за дорогой, вторым посматривал на меня с беспокойством. Да чего ему еще нужно?
Если подумать, у какого класса виртуальная таблица будет точно совпадать с таблицей класса-наследника? Конечно, у него самого!
Предположим, что в момент создания объекта мы точно знаем его тип. Тогда возьмем умный указатель std::unique_ptr с пользовательским чистильщиком (deleter).
struct Deleter {
void operator ()(Base *p) { del_(p); }
template <class T>
static Deleter Make() { return {.del_ = [](Base *p){
delete reinterpret_cast<T*>(p);
}}; }
std::function<void(Base *)> del_;
};

Вот такой, собранный на скорую руку, вполне сгодится для иллюстрации принципа работы. Когда мы будем создавать умный указатель, позовем и функцию Make с шаблонным параметром, который однозначно определит тип наследника. Тогда внутри функции создается и сохраняется в переменной del_ простейшая лямбда-функция, которая приводит указатель к правильному типу и удаляет объект не через Base*, а как A*. Придет время удалять объект, будет вызван operator(), там мы и задействуем del_.
std::unique_ptr<Base, Deleter> a {new A, Deleter::Make<A>()};

В результате удаления умного указатель объект будет уничтожен правильно.
Автомобиль резко остановился. Глухомань, лишь высокие сосны обступали дорогу.
- Скажу как есть, я не таксист, - вдруг сказал водитель, - я эйчар компании "Ведро", а это наш новый формат собеседования.
"Какой токсичный", - только и успел подумать я.
- По технической части у нас положительная обратная связь. Вернусь за вами совсем скоро.
Дверь открылась, я вылетел на обочину, машина дала по газам, обдав на прощание облаком пыли.
👍6🔥4🤯2🤣1
dude shall not live by std::mutex alone
Если долго сидеть на embedded проектах, то рано или поздно придется столкнуться с темпоральным аспектом системного бытия. Время - предмет сложный, заголовочный файл chrono тому подтверждение. Более того, разработчику обычно не хватает выразительности языка и стандартной библиотеки - и вот в проекте уже десять классов для описания временных моментов и отрезков. Сейчас же я вспомнил об этом, поскольку пришло время вернуться и реализовать застабленные ранее функции через слезы, боль и отвращение.
Некоторое время назад мы успешно реализовали std::mutex в системе FreeRTOS, используя для этого исключительно готовые местные примитивы.
Однако стоит только приоткрыть дверь базовому синхронизирующему примитиву, как в образовавшуюся щель тут же ломится вся его родня.
Первым так и просится на вивисекцию класс std::timed_mutex. Собственно, разница между обычным мьютексом и временным невелика, примерно как между суси и роллом: "та же херь, - там только присыпочка из икры на роллах..." Они оба приватно наследуются от __mutex_base, ибо стыдятся своего родства и не желают открыто заявлять об этом.
Класс обладает стандартным набором функций-членов: lock и unlock, try_lock. Но есть нюанс! timed_mutex может попробовать захватить мьютекс в течение некоторого времени. Отвечают за это методы try_lock_for() и try_lock_until().
Где-то была похожая функциональность... точно! Обычная функция захвата во FreeRTOS .
Обратим внимание на сигнатуру
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );

Функция xSemaphoreTake, она же xQueueSemaphoreTake возвращает pdTRUE в случае успеха или что-то иное, если захват не удался.
Первый аргумент xSemaphore - дескриптор семафора типа SemaphoreHandle_t, он же QueueHandle_t.
Второй аргумент - это время в неких системных или нервных тиках, в течение которого функция ждет.
Собственно, мы уже использовали эту же функцию в __gthread_mutex_lock, только влепили вместо таймаута значение portMAX_DELAY, чтоб ждала вечно, как Хатико.
Вариантов реализации в GCC есть несколько, условимся, что по умолчанию у нас определен макрос _GTHREAD_USE_MUTEX_TIMEDLOCK и не определен _GLIBCXX_USE_PTHREAD_MUTEX_CLOCKLOCK.
В этом случае занырнем поглубже в метод try_lock_until.
template<class Clock, class Duration>
bool try_lock_until(const std::chrono::time_point<Clock, Duration>& timeout_time);

Если захват и освобождение мьютекса осуществляется через знакомую нам пару функций __gthread_mutex_lock/unlock, то новые возможности требуют новой функции. Метод try_lock_until в итоге вызовет функцию __gthread_mutex_timedlock, которую реализуем, например так:
int __gthread_mutex_timedlock(__gthread_mutex_t *__mutex,
const __gthread_time_t *__abs_timeout) {
__gthread_time_t rtime {time_correction(__abs_timeout)};
TickType_t const ms {rtime.tv_sec * 1000U + rtime.tv_nsec / 1000000U};
BaseType_t result {xSemaphoreTake(__mutex->mutex, ms)};
return result == pdTRUE ? 0 : EINVAL;
}

где __mutex - это указатель на дескриптор мьтекса типа __gthread_mutex_t, который мы уже состряпали,
а __abs_timeout - значение времени, до которого нужно ждать. Здесь и спрятался рогатый.
Во-первых, этот тип еще надо определить.
using __gthread_time_t = struct timespec;

Почему timespec? На самом деле вариантов немного, поскольку создается переменная перед вызовом __gthread_mutex_timedlock весьма однозначно.
__gthread_time_t __ts = {
static_cast<std::time_t>(__s.time_since_epoch().count()),
static_cast<long>(__ns.count())
};

Во-вторых, важно понимать, что это абсолютное значение времени, грубо говоря. Поэтому нужно преобразовать его в значение относительного времени, иначе xSemaphoreTake будет ждать очень и очень долго. Для этого мы вызываем функцию time_correction, где получим текущее значение времени и вычтем его из __abs_timeout.
Да, чуть не забыл про try_lock_for. Иронично, но новые методы используют одну __gthread_mutex_timedlock, более того try_lock_for вообще реализуется через try_lock_until. Такие дела. Универсальные решения требуют жертв, товарищ!
👍52🔥2👏1
_gettimeofday
Последнюю неделю мне не давал покоя вопрос, является ли желание демонстрировать свой код незнакомым лицам в публичных местах проявлением девиантного поведения и можно ли это монетизировать? За разъяснением пришлось обратиться в некое лечебное заведение. Когда меня привезли, случайно подслушал разговор врача с пациентом. Просто если припасть ухом к замочной скважине, то слышимость изумительная.
- Доктор, я вот когда топором махаю долго, то запястья очень болят.
- Вы лесорубом работаете?
- Я тимлид...
- Ага. Это оттого, что "долго" - понятие относительное. Нужно знать точно! А точная работа со временем потребует тщательной подготовки. Можно добавить в код system_clock::now() или timed_mutex, но нужно переопределить вызов _gettimeofday!
Иначе сборка страшно ругается:
warning: _gettimeofday is not implemented and will always fail

Функция _gettimeofday важна. Вообще, в этих ваших линуксах существует системный вызов gettimeofday, который возвращает текущее время в секундах и микросекундах прошедших с начала эпохи Unix. В нашем проекте GCC везде для получения информации о времени использует эту функцию, будь то функция time(), либо system_clock::now().
int _gettimeofday(struct timeval *tv, void *tzvp);

Первый параметр - это указатель на структуру struct timeval.
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};

Второй аргумент - это, на самом деле, указатель на структуру struct timezone.
struct timezone {
int tz_minuteswest; // minutes west of Greenwich
int tz_dsttime; // type of DST correction
};

Возвращает же функция ноль в случае успеха и -1 при любом другом исходе.
Работать с разными часовыми поясами нужно, но мне пока неохота, да и запястья болят. А вот первый аргумент вызывает чувство дежавю, поскольку нечто такое я уже делал, только для прошивки на IAR. Там краеугольным камнем работы со временем была функция clock(). Однако это было давно и неправда, теперь рулит _gettimeofday. Свяжем в ней аппаратную часть, т.е. RTC с нашей программной системой. Через HAL получим значения RTC_DateTypeDef и RTC_TimeTypeDef, преобразуем результат в максимально похожую структуру tm, которую уже без труда преобразуем в time_t.
Если особо не принюхиваться к коду, то реализовать функцию можно довольно быстро:
extern "C"
int _gettimeofday(struct timeval *tv, void *) {
if (tv) {
RTC_DateTypeDef rtcDate {};
RTC_TimeTypeDef rtcTime {};
HAL_RTC_GetTime(&hrtc, &rtcTime, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, &rtcDate, RTC_FORMAT_BIN);
struct tm c_time{};
c_time.tm_year = 125 + rtcDate.Year; // since 2025
c_time.tm_mon = rtcDate.Month - 1; // tm_mon[0, 11] <?> Month[1, 12]
c_time.tm_mday = rtcDate.Date; // tm_mday[1, 31] <?> Date[1, 31]
c_time.tm_wday = rtcDate.WeekDay - 1; // tm_wday[0, 6] <?> WeekDay[1, 7]
c_time.tm_hour = rtcTime.Hours; // tm_hour[0, 23] <?> Hours[0, 23]
c_time.tm_min = rtcTime.Minutes; // tm_min[0, 59] <?> Minutes[0, 59]
c_time.tm_sec = rtcTime.Seconds; // tm_sec[0, 60] <?> Seconds[0, 59]
tv->tv_sec = mktime(&c_time);
tv->tv_usec = ((rtcTime.SecondFraction - rtcTime.SubSeconds) * 1000000) / (rtcTime.SecondFraction+1);
}
return 0;
}

Главное - помнить, что диапазоны HAL-структур отличаются от допустимых значений struct tm. Например, диапазон tm_year начинается от 0, который на самом деле представляет 1900 год, и до максимального значения переменной int, диапазон же Year зажат в пределах от 0 о 99 лет. Поэтому мы задаем смещение 125 лет, чтоб начать с 2025 года и протянуть аж до 2124 года, когда наконец-то подвезут мое новое кибернетическое тело...
Или вот месяц tm считает с нуля, как всякий нормальный человек. В RTC Month же начинает с единицы, потакая всяким нубам, которые искренне полагают январь первым месяцем. Насчет високосной секунды можно особо не переживать, а просто выставить таймер и вперед, ломать дрова!
👍5🤣1
std::recursive_mutex & std::recursive_timed_mutex
Снился мне старый офис. Я гордо шел по коридору с теннисной ракеткой в руке. Однако отнюдь не партия в теннис предстояла мне, а заседание в ватерклозете. В бывшем заводском здании была единственная уборная на этаже, одноместная и запирающаяся на ключ. Давным-давно сумрачный начальственный гений повелел присовокупить ракетку к ключу, чтоб сей ценный артефакт не потерялся и всегда возвращался на место.
Ворвавшись наконец в комнату счастья, я запер дверь на ключ и оставил его в замке, дабы подлые негодяи из соседнего офиса не прервали бесцеремонным образом общение с белым другом. Потом я случайно выпал в открытое окно, но раньше люди были крепче, и уже через пять минут снова был у двери афедронной, с ужасом понимая, что закрыта она изнутри. "Это дедлок, - подумал я с грустью. - Лучше бы вместо теннисной ракетки с ключом в комплекте шел std::recursive_mutex!"
Это еще одна разновидность мьютекса, специально для ситуаций, где использование обычного мьютекса приводит к взаимоблокировке, например, если удерживающая блокировку функция вызывает саму себя или иную функцию, где захватывается тот же мьютекс.
Поток, который владеет рекурсивным мьютексом, может делать lock несколько раз (но не бесконечное число, а то можно дозахватываться до std::system_error), для освобождения мьютекса нужно столько же раз сделать unlock.
На наше счастье FreeRTOS имеет в загашнике и такую функциональность. Принцип работы с рекурсивным мьютексом здесь почти не отличается от обычного, нужно только переопределить соответствующие функции. Создание мьютекса тривиально:
inline void __gthread_recursive_mutex_init_func(__gthread_recursive_mutex_t *__mutex) {
__mutex->mutex = xSemaphoreCreateRecursiveMutexStatic(&__mutex->mutexBuffer);
}

Захват мьютекса нужно делать через xSemaphoreTakeRecursive, со значением portMAX_DELAY на месте таймаута:
inline int __gthread_recursive_mutex_lock(__gthread_recursive_mutex_t *__mutex) {
BaseType_t const result {xSemaphoreTakeRecursive(__mutex->mutex, portMAX_DELAY)};
return result == pdTRUE ? 0 : EINVAL;
}

Попытка захвата:
inline int __gthread_recursive_mutex_trylock (__gthread_recursive_mutex_t *__mutex) {
BaseType_t const result {xSemaphoreTakeRecursive(__mutex->mutex, 0)};
return static_cast<int>(result == pdTRUE);
}

Освобождение:
inline int __gthread_recursive_mutex_unlock (__gthread_recursive_mutex_t *__mutex) {
BaseType_t const result {xSemaphoreGiveRecursive(__mutex->mutex)};
return result == pdTRUE ? 0 : EINVAL;
}

И тотальное уничтожение рекурсивного мьютекса:
inline int __gthread_recursive_mutex_destroy(__gthread_recursive_mutex_t *__mutex) {
vSemaphoreDelete(__mutex->mutex);
return 0;
}

Если есть два типа мьютексов std::timed_mutex и std::recursive_mutex, то появляется естественное желание скрестить их и посмотреть, что выйдет! В этот раз из разработчиков вышел примитив синхронизации - std::recursive_timed_mutex.
Фактически это рекурсивный мьютекс, для привязки ко времени нужно лишь реализовать одну специальную функцию:
inline int __gthread_recursive_mutex_timedlock (__gthread_recursive_mutex_t *__mutex,
const __gthread_time_t *__abs_timeout) {
__gthread_time_t rtime {time_correction(__abs_timeout)};
TickType_t const ms {
static_cast<TickType_t>(rtime.tv_sec * 1000U) +
static_cast<TickType_t>(rtime.tv_nsec / 1000000U)};
BaseType_t result {xSemaphoreTakeRecursive(__mutex->mutex, ms)};
return result == pdTRUE ? 0 : EINVAL;
}

Кажется, все. У нас есть полный набор мьютексов во FreeRTOS, с таким комплектом никаких ракеток не надо.
🔥10🗿1