Я не обобщаю, я подвожу к сложности реализации такой элементарной
сущности как условная переменная. Без которой нормально задачу не
решить. Да, её можно построить на базе пары семафоров, причём один
нужен в TLS (а в ThreadX есть TLS?), или можно сделать как в SDL,
по семафору на каждого ждуна (что реально плохо выглядит когда их
много -- я уже как-то писал про это). В данной задаче можно
схалявить и сделать недоделку (не являющуюся полноценной условной
переменной, но работающей) на базе RWlock и семафора, причём RWlock тоже по-человечески не сделать... только реализацию с приоритетом читающей стороны, а чтоб его сделать по-человечески нужна опять же условная переменная (рекурсия!) ...
Есть ряд краеугольных примитивов на которых всё стоит. Это в первую очередь атомарные операции: test-and-set, fetch-add, compare-and-swap. Без них вообще невозможно (хотя есть алгоритм Петерсона, но он на мой взгляд настолько страшен, что лучше даже не пытаться). И, кстати, в младших ARM'ах нет пары инструкций LDREX/STREX и реализуется только test-and-set. Опять же я уже писал, что ядро Linux из-за этого предоставляет костыли с подпорками для реализации остальных операций. А как дело обстоит в ThreadX опять же?
На атомарных операциях сверху стоят быстрые реализации примитивов синхронизации. Тот же семафор или мьютекс, разумеется если они используются в системе с общей памятью (а embedded OS все такие) можно реализовать эффективно, без системного вызова, в ситуациях, например, когда мьютекс не залочен на входе и не имеет ждуна на выходе, когда семафор не имеет ждуна: достаточно просто инкрементировать ячейку памяти. Это единицы тактов, максимум десятки тактов (если Linux с костылями). Или запросто тысячи, если реальный системный вызов, с сохраненим контекста и всем таким прочим.
Почему примитивы синхронизации должны быть быстрыми, надеюсь очевидно? Иначе все операции, где они задействованы, заметно замедляются, и не дай бог там ещё что-то вытворяют в цикле (а по другому не сделать, например, ибо порядок залочки мьютексов жёстко задан под страхом дедлока).
Наконец есть ряд краеугольных примитивов синхронизации на основе которых можно построить остальные. Собственно в Linux это ровно один примитив -- futex. Ячейка памяти, на которой можно ждать, и ядру можно сказать пробудить один или все потоки ожидающие на данной ячейке. Достаточно удобно. Имея атомарные операции сверху можно построить ту же условную переменную и мьютекс. На основе которых можно уже построить всё остальное: очереди, семафоры, флаги событий, таймеры и т.п. В обратную сторону -- сильно сложней. Построить условную переменную на основе мьютекса и семафоров -- сложно. Есть известная микрософтовская статья, где они предлагают всё новые реализации и находят всё новые недостатки...
Так же есть краеугольные функции OS, в частности TLS. Для реализации которого в MIPS даже аппаратный спец. регистр сделан. На ARMv6+ тоже. На ранних ARM, как я понимаю -- программно. А программно без поддержки OS тоже не просто. Можно, достаточно знать границы стеков всех потоков и на основе значения SP вычислить условно номер текущего потока и найти адрес local storage для данного потока. Но данная операция становится чрезвычайно тяжёлой. Особенно в ситуации, когда стеки потоков не фиксированные (например, все по 2МБайта начиная с такого-то адреса), а назначаемые произвольно (в Linux ещё есть sigaltstack...) Без TLS не реализовать ряд алгоритмов (да чего далеко ходить, элементарно errno из libc не работает). В частности условную переменную сделать сложней.
Но во многих embedded OS как под копирку реализуют какие-то одни функции и не реализуют остальные. Почему сделан именно такой выбор понять невозможно, кажется что просто список функций списывают с других ранних коммерческих RTOS. Есть мьютексы, семафоры, флаги событий (из TRON, но их обычно 32шт. и ни одним больше), очереди сообщений, откровенно плохой аллокатор памяти (и начинаются заумные разговоры про фрагментацию и необходимость пулов блоков фиксированных размеров -- при том, что если взять Doug Lea allocator, то оно как-то работает не хуже, а архитектуру ПО в рамках ограничений RTOS толком не выстроить). При этом все примитивы синхронизации как правило медленные, поддержка атомиков и TLS под вопросом (скорей никак), быстрые примитивы не реализовать, условную переменную реализовать сложно (а реализовать эффективно -- невозможно).
В итоге реализовать не предусмотренные в ОС примитивы, например имеющийся в Windows WaitForMultipleEvents() или в Linx select(), poll() -- невозможно очень сложно. А последний тоже своего краеугольный камень, без которого невозможна реализация да банально однопоточного веб-сервера (обрабатывающего в одном потоке параллельно множество соединений). Как ждать параллельно на нескольких сокетах? Какой-то аналог libevent тоже не построить. Вообще событийную систему, где событий достаточно много (больше 32 шт. предлагаемых в event flags), они потенциально разнородные (не только сокеты, но файлы, таймеры, пайпы, очереди сообщений...) и они обрабатываются в одном потоке. Да можно устроить какую-то дичь, когда создаются специальные потоки, которые ждут там чего-то одного и перекладывают всё в единую очередь сообщений (которая сама по себе может оказаться тяжёлая -- опять же вопрос реализации, в userspace или через системный вызов), из которой уже отдельный поток всё разбирает... потом можно задаться вопросом переполнения очереди сообщений, нужен какой-то механизм синхронизации того кто постит сообщения в очередь и того, кто их вынимает -- всё обрастает какими-то чудовищными подробностями.
Кстати, а как в ThreadX обстоят дела с обнаружением дедлоков? Во-первых дедлок, если он возник, нужно бы уметь обнаружить по факту его возникновения, во-вторых неплохо бы на основе тестового прогона построить граф задающий порядок лочки, обнаружить в нём циклы и все потенциальные, но практически редко возникаемые дедлоки. А то как вообще отлаживать? Ну и кстати полезно уметь для каждого потока вывести диагностическую информацию: где он заблокирован (на какой блокирующей функции, или каком конкретно примитиве синхронизации), какой другой поток владеет мьютексом на котором встал поток, бэктрейсы для каждого потока, наконец...
Возвращаясь к обсуждаемой проблеме. Вариант 1 у тебя очевидно бажный: буфер будет использоваться сильно не полностью, все будут стоять и ждать пока отправится в компорт одна строка, потом будет записана одна следующая. Хотя потенциально могли уместиться все, например. Принципиальная проблема -- нужно отношение один-к-многим. Один читающий поток должен уметь будить всех писателей. С одним семафором этого принципиально никак не сделать. Ты сейчас начнёшь изобретать, мол семафор и счётчик, я легко могу парировать кейсами когда это опять же не работает, и на примерно пятой итерации у тебя получится вариант из статьи: https://www.microsoft.com/en-us/research/publication/implementing-condition-variables-with-semaphores/ Там будет два семафора и необходимость реализации TLS (один семафор глобальный, второй специфичный для потока, и список ждунов ещё). Лучше ты не изобретёшь.
Вариант 2 вообще не вариант. Логгер должен быть *сверхбыстрым*. Это означает большой буфер и торможение только если его _изредка_ переполняет. Ибо логгер который порядочно тормозит программу никому не нужен. Потери текста в логгере тоже не приемлимы -- как потом баги решать? В идеале логгер должен быть без лочки вообще (невозможно в случае переполнения), или с очень маловероятными блокировками.