Ориентация главы

Том 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概念

Жизненный цикл потока

1СозданиеxrtThreadCreate(процедура, данные, размер_стека) -> дескриптор (размер 0 - платформенное умолчание)
2Исполнениесигнатура процедуры xthreadproc - возвращаемое значение и есть результат потока (int)
3СведениеxrtThreadWait(дескриптор) -> забрать результат и освободить - без Wait утечка
4ВспомогательныеCurrent/Yield/CurrentId - самосознание и уступка (Yield употреблён в примере TCP главы 65)

Дисциплина Wait — первый железный закон главы: каждый созданный поток сводится ровно один раз — не Wait это утечка ресурсов, двойной Wait не определён. Возвращаемое значение потока (источник вывода exit: 42) забирается через Wait — самая первичная передача результата между потоками.

Четвёрка примитивов

ПримитивСемантикаСтандартная сцена
Mutexпо одному; Lock/Unlock; TryLock неблокирующий щуплюбое изменение разделяемой структуры данных
ConditionWait спит до 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:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/concurrency/thread/main.c -lws2_32 -liphlpapiworker alpha: 13300exit: 42

Что только что произошло. (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:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/concurrency/sync/main.c -lws2_32 -liphlpapiprotected section

Что только что произошло. (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 (журналы с миллисекундными метками пересортировывают сцену гонки).

Типичные ошибки

Ловушка 1: забыть Wait или дважды Wait:Ловушка 1: забыть Wait или дважды Wait

Симптом: первое — утечка ресурсов потока (видно в статистике главы 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 только сводит */
Ловушка 2: перепутанный порядок блокировок до deadlock-а:Ловушка 2: перепутанный порядок блокировок до deadlock-а

Симптом: изредка взаимная зависание — поток 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).

Вызов: бенчмарк rw-блокировки

Одни и те же данные «много чтения, мало записи» (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 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки