Ориентация главы
Том 6 начинает стройку с самого нижнего слоя: потоки (xrtThreadCreate/Wait — трёхшаговый жизненный цикл создания, исполнения, сведения) и четвёрка примитивов синхронизации (Mutex, Condition, Semaphore, RWLock), плюс обещанная в главе 5 (золотой стандарт) отказоспособная однократная инициализация once. Позиция главы — «слой примитивов»: это сырьё параллельной системы, и большинству бизнес-кода не следует трогать примитивы напрямую (это территория Channel/Future/групп задач), но понимание примитивов — предпосылка понимания вышележащих обёрток и последняя точка опоры при диагностике параллельных проблем.
53.1引入
Три простых вопроса открывают путь. Вопрос первый: два потока наращивают один счётчик по десять тысяч раз каждый — почему результат не двадцать тысяч — count++ это чтение-изменение-запись, три шага, которые у двух потоков могут перемешаться; Mutex превращает участок в «по одному за раз». Вопрос второй: как потоку-потребителю ждать данных производителя — активное вращение жжёт CPU, опрос со sleep-ом добавляет задержку; Condition — механизм «уснуть, пока не разбудят». Вопрос третий: инициализация конфигурации медленная и может отказать, а она нужна многим потокам — пробовать инициализацию в каждом потоке и расточительно, и гонка; once гарантирует «ровно один поток исполняет, отказ можно повторить, остальные ждут результата».
Три вопроса дают три группы ответов и размечают разделение труда примитивов: Mutex ведает взаимоисключением (разделяемые данные — по одному), Condition ведает ожиданием (спать, пока условие не выполнится), Semaphore ведает квотой (не более N одновременно), RWLock ведает асимметрией чтения-записи (чтение совместное, запись монопольная). Цена неверного выбора примитива — deadlock или обвал производительности; в зоне ловушек главы по одному экземпляру каждого рода.
53.2概念
Жизненный цикл потока
Дисциплина Wait — первый железный закон главы: каждый созданный поток сводится ровно один раз — не Wait это утечка ресурсов, двойной Wait не определён. Возвращаемое значение потока (источник вывода exit: 42) забирается через Wait — самая первичная передача результата между потоками.
Четвёрка примитивов
| Примитив | Семантика | Стандартная сцена |
|---|---|---|
| Mutex | по одному; Lock/Unlock; TryLock неблокирующий щуп | любое изменение разделяемой структуры данных |
| Condition | Wait спит до Signal/Broadcast; в паре с Mutex | «данные пришли» производителя-потребителя |
| Semaphore | счётная квота; Wait списывает, Post пополняет | потолок пула соединений, управление конкурентностью |
| RWLock | чтение совместное, запись монопольная; много читателей, один писатель | конфигурация «много читают, мало пишут» |
Форма семейства у четвёрки единая: две формы жизни — инициализация на стеке (Init/Unit) или создание в куче (Create/Destroy), неблокирующие варианты Try, у ожидающих поддержка deadline (стык с xtime главы 42 — WaitFor/WaitUntil с тайм-аутом).Дисциплина пары Condition: Condition обязан работать с одним Mutex — перед Wait держать блокировку; внутри Wait атомарно «отпустить + уснуть», при пробуждении снова взять (ядро механизма; забыть про пару — неопределённое поведение).
once: отказоспособная однократная инициализация
Золотой стандарт главы 5 разбирал отказоспособную семантику once (bool (*xonceproc)(ptr) — возврат false позволяет повтор, XERR_STATE бережёт от рекурсии). Эта глава добавляет её место на параллельной карте: once — «замена Mutex для путей инициализации» — дешевле Mutex (безблокирующий быстрый путь) и семантически точнее («ровно один раз», а не «взаимоисключающий доступ»). Загрузка конфигурации, конструирование глобальных ресурсов, ленивые синглтоны — всё её территория.
Четыре условия deadlock-а и профилактика
Deadlock требует одновременного выполнения четырёх условий: взаимоисключение, «держу и жду», неперехватываемость, круговое ожидание. Инженерная профилактика бьёт самое лёгкое из последних двух: порядок блокировок — всей программой согласованный порядок захвата (скажем, «сперва A, потом B») — и круговое ожидание исчезает структурно; эскалация TryLock — вторую блокировку брать через Try, при неудаче отпустить удержанное и повторить (ломает «держу и жду»). Последнее средство уровня четвёрки — тайм-аут: Wait с deadline; превышение это диагностика (форма обнаружения deadlock-а в рантайме).
53.3示例
Полная программа: создание и сведение потоков
Из образца репозитория examples/concurrency/thread/main.c:
#include <stdio.h> #include <xrt.h> */ /* 在线程中执行一个短任务。 */ static int32 worker(ptr pData) { cstr sName = (cstr)pData; printf("worker %s: %llu\n", sName, (unsigned long long)xrtThreadCurrentId()); return 42; } /* 创建、等待并释放线程。 */ int main(void) { xthread* pThread = xrtThreadCreate(worker, (ptr)"alpha", 0); if ( pThread == NULL ) { return 1; } if ( xrtThreadWait(pThread) != XWAIT_OK ) { xrtThreadDestroy(pThread); return 1; } printf("exit: %d\n", xrtThreadExitCode(pThread)); xrtThreadDestroy(pThread); return 0; }
Что только что произошло. (1) xrtThreadCreate принимает тройку: процедуру (сигнатура xthreadproc — возврат int32(ptr)), данные (ptr напролёт — здесь строка имени) и размер стека (0 — платформенное умолчание; при глубокой рекурсии задаётся явно). (2) Рабочий поток печатает имя и ID потока (13300 — меняется от запуска к запуску); (3) главный поток сводит через xrtThreadWait — xwaitresult сообщает исход ожидания; возвращаемое значение потока (здесь 42) передаётся через локальное для потока соглашение или Future (глава 58), а exit: 42 образец берёт сам из атомарной переменной — Wait отвечает за сведение, а не за взятие значения; (4) после Wait дескриптор уже освобождён — Destroy не нужен (и невозможен).
Полная программа: мьютекс защищает критическую секцию
Из examples/concurrency/sync/main.c:
#include <stdio.h> #include <xrt.h> */ /* 展示嵌入式同步对象的基础用法。 */ int main(void) { xmutex tMutex; if ( !xrtMutexInit(&tMutex) ) { return 1; } (void)xrtMutexLock(&tMutex); printf("protected section\n"); (void)xrtMutexUnlock(&tMutex); (void)xrtMutexUnit(&tMutex); return 0; }
Что только что произошло. (1) Mutex Init — инициализация на стеке (ноль распределений — тот же выбор, что и у контейнерной пары куча/стек). (2) Между Lock и Unlock — критическая секция: два Lock на один Mutex у двух потоков взаимоисключают, и код секции исполняется как без помех (protected section печатается ровно один раз, не разорванный). (3) Образец sync_tour — полный обход интерфейсов четвёрки: Try у Mutex (щуп блокировки), Wait/WaitFor у Condition (ожидание с тайм-аутом в паре с Signal), квотная семантика Semaphore (после post-many=3 три Wait проходят все) — четыре строки ok на четыре примитива. Образцы condition и rwlock разворачивают полные случаи условного ожидания и rw-блокировки.
53.4契约
- Балансировка потоков: один Wait на поток (возвращает xwaitresult); без Wait — утечка, двойной Wait не определён; возвращаемое значение — через атомики или Future, Wait только сводит.
- Минимальная критическая секция: между Lock и Unlock только то, что обязано быть взаимоисключающим — IO, журналы, распределения по возможности выносить.
- Пара Condition: обязан работать с Mutex; перед Wait держать блокировку; Broadcast для многих потребителей выбирается явно.
- Дисциплина порядка блокировок: в многоблочной программе единый порядок захвата; или стратегия эскалации TryLock — одно из двух, письменно в документацию модуля.
- Семантика once: ровно один раз + повтор при отказе +
XERR_STATEот рекурсии (контракт главы 5 дословно в силе). - Диагностика по тайм-ауту: ожидающие примитивы с deadline — превышение это сигнал deadlock-а, а не повод молча повторять.
Позиционирование слоя примитивов: когда четвёрка не нужна
Рассказывая о примитивах, надо рассказать, когда их не употреблять — три признака прямого касания примитивов бизнес-кодом.Признак один: Mutex охраняет «передачу задач» — два потока передают данные ручной сборкой блокировка + условная переменная, а это территория Channel (глава 57): очередь + пробуждение + противодавление за один шаг.Признак два: из потока + Wait лепится «асинхронный результат» — поток создаётся, чтобы посчитать значение и взять его Wait-ом, а это территория Future/пула задач (главы 58/59): подал — получил Future, потоки переиспользуются пулом.Признак три: Semaphore регулирует «степень конкурентности» — ограничение частоты на семафоре работает, но глубина очереди пула задач и есть предел конкурентности (плюс противодавление). Общий критерий трёх признаков: примитивы решают «как синхронизироваться», верхние слои — «как сотрудничать» — писать логику сотрудничества, держа в руках примитивы, значит заново строить верхний слой. Легитимное прямое употребление примитивов: защита чистых структур данных (кеш, счётчики), критичные к производительности пути (где накладные расходы блокировки заметны), клей на границе с внешними потоковыми библиотеками.
Разделение с очередями главы 21 и атомиками главы 9
Трёхслойный ящик инструментов для конкурентного доступа легко перепутать; одно сравнение всё расставляет.Атомные операции (глава 9): чтение-запись одной переменной — счётчики, флаги, указатели; без блокировок, но только одна переменная.Mutex/четвёрка (эта глава): поддержка инвариантов многих переменных — ограничения вида «два поля обязаны меняться вместе» атомам не по зубам, их берут блокировки.Безблокировочные очереди (глава 21): передача указателей SPSC/MPSC/MPMC — внутри очереди без блокировок, вам блокировок добавлять не нужно. Выбор слоя определяет «форма разделения»: одна переменная → атомики, инварианты → блокировки, передача указателей → очереди. Типичные ошибки смешения слоёв — обернуть Mutex поверх очереди (очередь уже потокобезопасна) или блокировать счётчик (атомики дешевле); сознание слоёв изоморфно выбору контейнеров главы 23.
Взгляд на тестирование: детерминированные тесты параллельности
Свойство параллельного кода «проявляется раз на десять тысяч прогонов» превращает тестирование в мистику. Три инженерных средства.Дожать: окна гонок раскрываются под нагрузкой — тесты гоняют полное число потоков + искусственные уступки (Yield, инъецирующий перемешивание) + долго (ночные задания CI).Понизить размерность утверждений: промежуточные состояния в произвольные моменты не утверждаемы; утверждения ложатся только после точек сведения (все Wait завершились — глобальная проверка) — «в итоге согласовано» проверяемо на порядок лучше, чем «согласовано на каждом шаге».Поддержка инструментами: санитайзеры класса TSan на отдельной дорожке CI (медленно, но ловит настоящие гонки); внедрение отказов главы 6 закрывает OOM-сценарии на путях блокировок. Реалистическое ожидание от трёх вместе: параллельный тест доказывает «проблема не найдена», а не «проблемы нет» — локализация остаётся за конвейером наблюдения главы 50 (журналы с миллисекундными метками пересортировывают сцену гонки).
Типичные ошибки
Симптом: первое — утечка ресурсов потока (видно в статистике главы 6); второе — изредка крах или перепутанные возвращаемые значения.
Причина: дескриптор потока — «обещание, которое обязано быть сведено»: создать — задолжать один Wait; Wait — исполнение обещания, второй — двойное исполнение.
xthread* Worker = xrtThreadCreate(work, NULL, &Config); /* ... главный поток вышел без Wait - Worker утёк; хуже: при выходе процесса поток ещё исполняется */
xthread* Worker = xrtThreadCreate(work, NULL, 0); if ( xrtThreadWait(Worker) != XWAIT_OK ) { /* ровно один раз */ return false; } /* возвращаемое значение потока идёт через атомик/Future/очередь - Wait только сводит */
Симптом: изредка взаимная зависание — поток 1 держит A и ждёт B, поток 2 держит B и ждёт A; на малой конкурентности не проявляется, в стресс-тестах изредка, в продакшене неизбежно.
Причина: два места кода берут одну пару блокировок в противоположных порядках — четыре условия кругового ожидания собраны.
/* код потока 1 */ /* код потока 2 */ xrtMutexLock(&A); xrtMutexLock(&B); xrtMutexLock(&B); xrtMutexLock(&A); /* порядок обратный - окно deadlock-а */ Transfer(A, B); Transfer(B, A); xrtMutexUnlock(&B); xrtMutexUnlock(&A); xrtMutexUnlock(&A); xrtMutexUnlock(&B);
/* общепрограммная конвенция: когда нужны и A, и B - всегда сперва A, потом B (в документацию модуля и чек-лист ревью) */ /* поток 1 и поток 2 оба приводятся к виду: */ xrtMutexLock(&A); /* A первой - конвенция */ xrtMutexLock(&B); /* B второй */ Transfer(A, B); xrtMutexUnlock(&B); xrtMutexUnlock(&A);
Практические задания
Два потока наращивают безблокировочный счётчик по миллиону раз каждый против версии под Mutex — сравните результаты (безблокировочная версия при каждом прогоне своя и меньше двух миллионов); добавьте третью версию на атомных операциях (глава 9) — трёхстороннее сравнение.
Condition + Mutex реализуют производство и потребление над ограниченной очередью: производитель ждёт при заполнении, потребитель при пустоте, каждый будит другого — классическое упражнение с двумя условными переменными (фундамент — очередь главы 21).
Одни и те же данные «много чтения, мало записи» (90% чтений/10% записей) под Mutex и под RWLock; четыре потока конкурентно исполняют сто тысяч операций — сравните пропускную способность. Критерии приёмки: версия RWLock заметно выше версии Mutex (дивиденд совместного чтения); голодания писателя нет (или устранено стратегией приоритета записи); цифры в сравнительную таблицу.
Итоги
| Тема | Кратко |
|---|---|
| Три шага потока | Create (процедура + данные + конфигурация) → исполнение → Wait (взять результат + освободить, ровно один раз) |
| Четвёрка | Mutex взаимоисключение / Condition парное ожидание / Semaphore квота / RWLock чтение совместно, запись монопольно |
| Формы жизни | Init/Unit на стеке без распределений или Create/Destroy в куче - в согласии с конвенциями контейнеров |
| Семейство тайм-аутов | WaitFor/WaitUntil с deadline - тайм-аут это сигнал deadlock-а, а не сигнал повтора |
| once | ровно один раз + повтор при отказе + защита от рекурсии - замена Mutex на путях инициализации |
| Профилактика deadlock-а | конвенция порядка блокировок или эскалация TryLock - одно из двух, письменно |
| Дисциплина критической секции | минимизировать - IO/журналы/распределения вне секции |
📖 Справочники API в этой главе:thread (29 записей)sync (50 записей)atomic (30 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки