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

Этой главой том 1 закрывается. Предыдущие восемь глав строили фундамент однопоточного мира: типы и представления (глава 3), модель ошибок (глава 4), управление памятью (главы 5–6), временные арены (глава 7), версии и обрезка (глава 8). Эта глава добавляет последний кусок: что удерживает порядок, когда несколько потоков исполнения трогают одни данные на самом нижнем уровне. Три ступени: атомарные операции (xatomic32/64/ptr — чтение, запись и чтение-изменение-запись одной переменной без чередования); спин-блокировки (xspinlock — защита критических секций длиной в несколько инструкций); примитивы ожидания (int64 и xwaitresult — математика тайм-аутов и единый словарь исходов ожидания).

Это классическая глава-фундамент. Глава 8 упоминала, что заголовок атомарных операций — один из первых потребителей компиляционного развилки возможностей: один и тот же заголовок на платформах без атомарных инструкций идёт по внутренней реализации, а где они есть — по инлайн-пути, благодаря замыканию возможностей прямо здесь. Смотря вперёд: синхронизация потоков главы 53, система отмены главы 54 и «атомарная замена глобального указателя» центра конфигурации (главы 139–140) всё стоит на примитивах этой главы; здесь преподаётся только слой примитивов — одна машина, одна переменная, осязаемые мелочи; системный конкурентный дизайн остаётся тому 6. Прочитав, вы должны уметь отвечать на три вопроса: когда блокировка не нужна (достаточно одной переменной); когда блокировка не должна быть тяжёлой (счёт порядков величин «спин против мьютекса»); и почему «ждать до тайм-аута» обязано использовать абсолютный дедлайн, а не относительный тайм-аут, сбрасываемый каждый круг.

9.1引入

Три реальных инцидента открывают путь. Первый: счётчик теряет обновления. Сервис статистики копит число запросов через count++; восемь потоков добавляют по миллиону, сумма останавливается около 7,2 миллиона и при каждом запуске разная. count++ — чтение-изменение-запись из трёх шагов; три шага двух потоков могут чередоваться — оба читают 41, каждый прибавляет единицу, оба пишут 42 — одно обновление бесследно исчезает. Мьютекс лечит, но платить «сном-пробуждением» за инкремент целого — из пушки по воробьям. Второй: флаг лжёт. Поток-производитель заполняет структуру, затем выставляет флаг «данные готовы»; потребитель изредка видит флаг истинным, а читает полуновые данные. Записи без ограничений порядка не дают межпеременных обещаний — видимость флага не влечёт видимость данных. Это не вероятностная мистика, это модель памяти. Третий: суммарное время цикла повторов выходит из-под контроля. Код «ждать 100 мс на каждую попытку, сдаться через десять секунд» реально работал двадцать восемь секунд — каждый круг ждал свежие 100 мс, бюджет ожидания сбрасывался десятки раз. Системе нужно не «сколько ждать каждый круг», а «до какого момента».

Три проблемы — три инструмента: атомарное чтение-изменение-запись делает count++ одним шагом; порядок памяти делает пару «публикация-получение» надёжной; математика дедлайнов превращает тайм-аут из относительной величины в абсолютный момент времени. Вместе их меньше тридцати функций, но это общий фундамент всего в книге — от очередей (глава 21) до токенов отмены (глава 54).

9.2概念

Три ширины и семейство чтения-изменения-записи

Атомарный модуль даёт три ширины: xatomic32, xatomic64, xatomicptr — целые 32/64 бита и ширина одного указателя. Семейная форма у всех одинакова: Init времени исполнения (статические объекты — макросами вроде XRT_ATOMIC32_INIT(0u), стартовое состояние одинаково на всех платформах), Load, Store, Exchange, FetchAdd/FetchSub/FetchAnd/FetchOr/FetchXor и CompareExchange. Префикс «Fetch» единообразно значит вернуть старое значение: xrtAtomic64FetchAdd(&Counter, 1u, ...) возвращает значение до прибавления, а счётчик уже обновлён — нужно новое? Прибавьте единицу к старому; не возвращайтесь за ещё одним Load (лишняя атомарная операция и лишнее предположение о времени).

Полезное правило: форму разделения выбирает инструмент. Просто счётчик, флаг, указатель «на кого это сейчас указывает» — одна переменная; атомарных достаточно, без блокировок и дёшево. «Два поля должны меняться вместе» — инвариант, который атомарность не выразит (между двумя атомарными операциями всё ещё можно вклиниться); это территория блокировок (глава 53). Передача владения указателем между потоками — территория очередей (глава 21). Классические симптомы неверного слоя: мьютекс поверх очереди (очередь уже потокобезопасна), блокировка ради счётчика (атомарные дешевле на порядок).

Сравнение с обменом: контракт переписи ожидаемого значения

xrtAtomic64CompareExchange(&Counter, &iExpected, 10u, 成功序, 失败序) (порядок успеха, порядок неудачи) заслуживает отдельного раздела больше всех в семействе: если текущее значение равно *iExpected — поменять на 10u и вернуть истину; если нет — вернуть ложь и переписать фактически наблюдённое текущее значение в *iExpected. Эта перепись — намеренный контракт: стандартная форма CAS-цикла — «попробовать с ожидаемым; при неудаче поправиться переписанным фактическим значением и повторить», без дополнительного Load и без лишнего окна между чтением ожидания и новой попыткой. Порядок неудачи поэтому обязан нести семантику получения (переписанное значение должно читаться), не быть сильнее порядка успеха и не содержать RELEASE — проверка параметров это принуждает; незаконная комбинация ставит XERR_ARGUMENT, объект не тронут.

Классическое применение CAS — «изменить одно поле структуры, не блокируя всю структуру»: прочитать старое, вычислить новое локально, CAS-нуть обратно; неудача значит, кто-то опередил — повторить с переписанным значением. Каждый шаг цикла атомарен, всё «чтение-вычисление-запись» — нет: это и есть граница применимости: при редких конфликтах почти всегда успех с первой попытки; при плотных конфликтах сам цикл становится горячей точкой. Очередь без блокировок главы 21 — этот же паттерн, высоко инженеризованный.

Порядок памяти: пять уровней и одно правило по умолчанию

Каждая атомарная операция принимает параметр xmemoryorder: XMEMORY_RELAXED (атомарна только эта операция; никакого межпеременного порядка — для чистого счёта достаточно); XMEMORY_ACQUIRE (сторона чтения — последующие обращения не переупорядочиваются до неё; «получить» опубликованные данные); XMEMORY_RELEASE (сторона записи — предыдущие обращения не переупорядочиваются после неё; «опубликовать» завершённые записи); XMEMORY_ACQ_REL (оба, для чтения-изменения-записи — стандартная поза CAS); XMEMORY_SEQ_CST (полная последовательная согласованность — самое наглядное и самое дорогое). Матрица законности проста: Load принимает только RELAXED/ACQUIRE/SEQ_CST, Store — только RELAXED/RELEASE/SEQ_CST; нарушение ставит XERR_ARGUMENT.

Инженерное правило — одна строка: пока не доказано обратное, используйте SEQ_CST. Ловушка порядка памяти в том, что ошибка по большей части работает — переупорядочивания и видимость всплывают лишь на конкретных CPU и уровнях оптимизации компилятора; зелёный тест не значит правильно. Верный рабочий процесс: сначала напишите правильно на SEQ_CST; когда профилирование (метод главы 136) укажет горячий счётчик или точку публикации — деградируйте с обоснованием до RELAXED или пары ACQUIRE/RELEASE и напишите в комментарии, почему это безопасно. Обещание платформы одностороннее: платформы только с полными барьерами реализуют более сильным порядком — публичный контракт никогда не ослабляется; опирайтесь на документированный нижний предел.

Пара «публикация-получение» — самая используемая: сторона записи Store(标志, 1, RELEASE) (флаг) (данные полностью готовы до объявления), сторона чтения Load(标志, ACQUIRE) (флаг) (данные, прочитанные после флага, неизбежно объявленной версии). Если любая сторона применила RELAXED — обещание испаряется; это герой ловушки 1 ниже.

Барьеры, Pause и запрос свободы от блокировок

Три помощника дополняют семейство: xrtAtomicThreadFence(序) (порядок) — самостоятельный барьер (не привязан к переменной — подстраховка смешанных форм вроде «несколько обычных записей плюс один атомарный флаг»); xrtAtomicSignalFence ограничивает только переупорядочивание компилятором, не CPU (граница «обработчик сигнала против потока»); xrtAtomicPause() — подсказывающая инструкция в зазоре вращения (PAUSE на x86, YIELD на ARM) — говорит конвейеру «этот цикл ждёт другого; не вливайте ресурсы спекулятивного исполнения сюда». xrtAtomicIsLockFree(sizeof(uint64)) отвечает «действительно ли эта ширина свободна от блокировок на вашей платформе» — один вопрос на этапе выбора закрывает большинство соображений порядка величины.

Спин-блокировка: критическая секция в несколько инструкций

xspinlock построена на атомарном CAS и является самой лёгкой блокировкой библиотеки: не удалось взять — занято ждёт (вращается), не спит. Применимость — счёт порядков величины: для критической секции в несколько инструкций вращение стоит одного неудавшегося CAS плюс нескольких Pause; мьютекс потенциально стоит системного вызова и переключения потоков — обычно на порядок дороже. Наоборот: как только секция содержит IO, выделение памяти или больше нескольких десятков инструкций, вращение из дешёвого превращается в сжигание CPU — вращающийся поток не только не работает, он отбирает ядра у держателя. Критерий — длина критической секции, а не «спин звучит продвинуто».

Три формы жизненного цикла изоморфны четвёрке главы 53: стек xrtSpinInit/xrtSpinUnit, статическая XRT_SPIN_INIT, куча xrtSpinCreate/xrtSpinDestroy; xrtSpinTryLock — неблокирующий вариант (возвращает ложь при занятости). Одно жёсткое контрактное правило: уничтожение удерживаемой блокировки завершается неудачей и ставит XERR_STATE — это не совет по очистке, а принудительная проверка от утечек.

1Разделение одной переменной (счётчик / флаг / указатель) -> атомарные операциибез блокировок, один шаг, чередование безопасно
2Инварианты многих переменных (поля меняются вместе) -> блокировкиспин (несколько инструкций) или мьютекс (дольше)
3Передача владения указателем между потоками -> очереди (глава 21)внутри уже без блокировок, снаружи не запирать

Примитивы ожидания: математика дедлайнов и пятирезультатный исход

Интерфейсы ожидания принимают относительный интервал в миллисекундах типа int64; XRT_WAIT_FOREVER означает неограниченное ожидание. Разность показаний xrtTimer() типа double позволяет рассчитать оставшееся время общего бюджета.

xwaitresult раскладывает «исход ожидания» на пять взаимоисключающих значений: XWAIT_ERROR (настоящая неудача), XWAIT_OK (успех), XWAIT_TIMEOUT (время вышло), XWAIT_CANCELLED (отменено — токены отмены главы 54 кончаются здесь), XWAIT_CLOSED (ожидаемый объект закрыт). Замысел: отделить нормальный поток управления от ошибок — тайм-аут и отмена ожидаемы ветви, которые обрабатывает вызывающий, а не «исключения», запихнутые в цепочку ошибок. От xrtThreadWait главы 53 до ожиданий соединений сетевой библиотеки возвращается именно это перечисление — выучите семантику здесь; систематизированное ожидание и отмена разворачиваются в томе 6.

9.3示例

Первая полная программа: атомарный счёт и однозначный CAS

examples/core/atomic/main.c源码 ↗

















 */

#include <stdio.h>
#include <xrt.h>



int main(void)
{
	/* 原子变量必须用 INIT 宏静态初始化,保证所有平台起始状态一致。 */
	xatomic64 Counter = XRT_ATOMIC64_INIT(0u);

	





	 */
	uint64 iExpected = 1u;

	/* 原子自增:返回旧值 0(丢弃),计数器变为 1;纯计数用 RELAXED 即可。 */
	(void)xrtAtomic64FetchAdd(&Counter, 1u, XMEMORY_RELAXED);

	



	 */
	if (
		xrtAtomic64CompareExchange(
			&Counter,
			&iExpected,
			10u,
			XMEMORY_ACQ_REL,
			XMEMORY_ACQUIRE
		)
	) {
		/* 读取最终值 10;没有其他生产者在发布数据,RELAXED 足够。 */
		printf("counter=%llu\n", (unsigned long long)xrtAtomic64Load(&Counter, XMEMORY_RELAXED));
	}
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/core/atomic/main.c -lws2_32 -liphlpapicounter=10

Что только что произошло. (1) Макроинициализация XRT_ATOMIC64_INIT(0u) — статические и стековые объекты одинаково; стартовое состояние согласовано между платформами. (2) FetchAdd(1, RELAXED) показывает: слабейшего порядка достаточно для чистого счёта — не публикуем других данных, не нужны и обещания порядка. (3) Ожидание CAS равно 1 (совпадает с фактическим значением после FetchAdd); успех меняет на 10 — при успехе *iExpected не тронут. (4) Пара «успех ACQ_REL / неудача ACQUIRE» — стандартная поза CAS: порядок неудачи обязан позволять чтение переписанного значения и не быть сильнее порядка успеха. Поменяйте iExpected на 2 и перезапустите: CAS не удаётся, перепись возвращает 1 — это значение и есть стартовая точка повтора.

Вторая полная программа: тур по семейству RMW

examples/core/atomic_tour/main.c源码 ↗






















 */

#include <stdio.h>
#include <stdint.h>
#include <xrt.h>

int main(void)
{
	xatomic32 A32;
	xatomic64 A64;
	xatomicptr APtr;
	uint32 iOld32 = 0;
	uint32 iOldAnd = 0;
	uint32 iOldOr = 0;
	uint32 iExpected = 0;
	uint64 iOld64 = 0;
	ptr pOld = NULL;
	ptr pExpected = NULL;
	static uint8 s_Target[2];
	int iResult = 1;

	/* ---- 32 位:Exchange / And / Or / Xor ---- */
	xrtAtomic32Init(&A32, 0x0Fu);
	iOld32 = xrtAtomic32Exchange(&A32, 0x3Cu, XMEMORY_ACQ_REL);
	if ( (iOld32 != 0x0Fu) ||
		(xrtAtomic32Load(&A32, XMEMORY_ACQUIRE) != 0x3Cu) ) {
		goto Cleanup;
	}
	iOldAnd = xrtAtomic32FetchAnd(&A32, 0x0Fu, XMEMORY_ACQ_REL);
	if ( iOldAnd != 0x3Cu ) {
		goto Cleanup;
	}
	if ( xrtAtomic32Load(&A32, XMEMORY_ACQUIRE) != 0x0Cu ) {
		goto Cleanup;  /* 0x3C & 0x0F = 0x0C */
	}
	iOldOr = xrtAtomic32FetchOr(&A32, 0x06u, XMEMORY_ACQ_REL);
	if ( iOldOr != 0x0Cu ) {
		goto Cleanup;
	}
	if ( xrtAtomic32Load(&A32, XMEMORY_ACQUIRE) != 0x0Eu ) {
		goto Cleanup;  /* 0x0C | 0x06 = 0x0E */
	}
	iOld32 = xrtAtomic32FetchXor(&A32, 0x0Bu, XMEMORY_ACQ_REL);
	if ( (iOld32 != 0x0Eu) ||
		(xrtAtomic32Load(&A32, XMEMORY_ACQUIRE) != 0x05u) ) {
		goto Cleanup;  /* 0x0E ^ 0x0B = 0x05 */
	}
	printf("atomic: 32-bit exchange=0F and/or/xor = %X/%X/%X\n",
		(unsigned)iOld32 & 0xFu,
		0xCu, 0x5u);

	/* ---- 32 位 CAS:成功路径 + 强语义失败回写 ---- */
	iExpected = 0x05u;
	if ( !xrtAtomic32CompareExchange(&A32, &iExpected, 0x0Au,
			XMEMORY_ACQ_REL, XMEMORY_ACQUIRE) ||
		(xrtAtomic32Load(&A32, XMEMORY_ACQUIRE) != 0x0Au) ) {
		goto Cleanup;
	}
	iExpected = 0x99u;  /* 期望错误:失败并把实际值 0x0A 写回 */
	if ( xrtAtomic32CompareExchange(&A32, &iExpected, 0x0Bu,
			XMEMORY_ACQ_REL, XMEMORY_ACQUIRE) ||
		(iExpected != 0x0Au) ) {
		goto Cleanup;
	}
	printf("atomic: cas ok=1 fail-rewrites=%X\n",
		(unsigned)iExpected);

	/* ---- 32 位:Store / FetchAdd / FetchSub(加减按模回绕) ---- */
	xrtAtomic32Store(&A32, 0x33u, XMEMORY_RELEASE);
	iOld32 = xrtAtomic32FetchAdd(&A32, 0x0Cu, XMEMORY_ACQ_REL);
	if ( (iOld32 != 0x33u) ||
		(xrtAtomic32Load(&A32, XMEMORY_ACQUIRE) != 0x3Fu) ) {
		goto Cleanup;
	}
	iOld32 = xrtAtomic32FetchSub(&A32, 0x0Fu, XMEMORY_ACQ_REL);
	if ( (iOld32 != 0x3Fu) ||
		(xrtAtomic32Load(&A32, XMEMORY_ACQUIRE) != 0x30u) ) {
		goto Cleanup;
	}

	/* ---- 64 位:Init / Exchange / Sub / And / Or / Xor ---- */
	xrtAtomic64Init(&A64, UINT64_C(100));
	iOld64 = xrtAtomic64Exchange(&A64, UINT64_C(50),
		XMEMORY_ACQ_REL);
	if ( iOld64 != UINT64_C(100) ) {
		goto Cleanup;
	}
	iOld64 = xrtAtomic64FetchSub(&A64, UINT64_C(20),
		XMEMORY_ACQ_REL);
	if ( (iOld64 != UINT64_C(50)) ||
		(xrtAtomic64Load(&A64, XMEMORY_ACQUIRE) != UINT64_C(30)) ) {
		goto Cleanup;
	}
	iOld64 = xrtAtomic64FetchAnd(&A64, UINT64_C(0x1C),
		XMEMORY_ACQ_REL);
	if ( (iOld64 != UINT64_C(30)) ||
		(xrtAtomic64Load(&A64, XMEMORY_ACQUIRE) != UINT64_C(28)) ) {
		goto Cleanup;  /* 30 & 28 = 28 */
	}
	iOld64 = xrtAtomic64FetchOr(&A64, UINT64_C(3),
		XMEMORY_ACQ_REL);
	if ( (iOld64 != UINT64_C(28)) ||
		(xrtAtomic64Load(&A64, XMEMORY_ACQUIRE) != UINT64_C(31)) ) {
		goto Cleanup;  /* 28 | 3 = 31 */
	}
	iOld64 = xrtAtomic64FetchXor(&A64, UINT64_C(31),
		XMEMORY_ACQ_REL);
	if ( (iOld64 != UINT64_C(31)) ||
		(xrtAtomic64Load(&A64, XMEMORY_ACQUIRE) != 0u) ) {
		goto Cleanup;  /* 31 ^ 31 = 0 */
	}
	xrtAtomic64Store(&A64, UINT64_C(7), XMEMORY_RELEASE);
	if ( xrtAtomic64Load(&A64, XMEMORY_ACQUIRE) != UINT64_C(7) ) {
		goto Cleanup;
	}
	printf("atomic: 64-bit init/exchange/sub/and/or/xor = %llu\n",
		(unsigned long long)xrtAtomic64Load(&A64,
			XMEMORY_ACQUIRE));

	/* ---- 指针:Init / Exchange / CompareExchange ---- */
	xrtAtomicPtrInit(&APtr, (ptr)&s_Target[0]);
	pOld = xrtAtomicPtrExchange(&APtr, (ptr)&s_Target[1],
		XMEMORY_ACQ_REL);
	if ( (pOld != (ptr)&s_Target[0]) ||
		(xrtAtomicPtrLoad(&APtr, XMEMORY_ACQUIRE) !=
			(ptr)&s_Target[1]) ) {
		goto Cleanup;
	}
	pExpected = (ptr)&s_Target[0];  /* 错误期望:失败回写 */
	if ( xrtAtomicPtrCompareExchange(&APtr, &pExpected,
			(ptr)&s_Target[0], XMEMORY_ACQ_REL,
			XMEMORY_ACQUIRE) ||
		(pExpected != (ptr)&s_Target[1]) ) {
		goto Cleanup;
	}
	pExpected = (ptr)&s_Target[1];
	if ( !xrtAtomicPtrCompareExchange(&APtr, &pExpected,
			(ptr)&s_Target[0], XMEMORY_ACQ_REL,
			XMEMORY_ACQUIRE) ) {
		goto Cleanup;
	}
	/* Store:CAS 后直接写回并核对。 */
	xrtAtomicPtrStore(&APtr, (ptr)&s_Target[1], XMEMORY_RELEASE);
	if ( xrtAtomicPtrLoad(&APtr, XMEMORY_ACQUIRE) !=
		(ptr)&s_Target[1] ) {
		goto Cleanup;
	}
	printf("atomic: ptr exchange/cas = ok\n");

	/* ---- 杂项:无锁判定 / 栅栏 / Pause ---- */
	if ( !xrtAtomicIsLockFree(sizeof(uint32)) ||
		!xrtAtomicIsLockFree(sizeof(uint64)) ) {
		goto Cleanup;
	}
	xrtAtomicThreadFence(XMEMORY_ACQ_REL);
	xrtAtomicSignalFence(XMEMORY_SEQ_CST);
	xrtAtomicPause();
	printf("atomic: lockfree(4/8)=1 fence+pause ok\n");
	iResult = 0;

Cleanup:
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/core/atomic_tour/main.c -lws2_32 -liphlpapiatomic: 32-bit exchange=0F and/or/xor = E/C/5atomic: cas ok=1 fail-rewrites=Aatomic: 64-bit init/exchange/sub/and/or/xor = 7atomic: ptr exchange/cas = okatomic: lockfree(4/8)=1 fence+pause ok

Что только что произошло. (1) Все три ширины проходят Exchange и побитовое семейство — «Fetch возвращает старое» — прямое доказательство в каждом утверждении (FetchAnd возвращает 0x3C, а новое 0x0C — его И с маской). (2) Вторая строка вывода посвящена переписи при неудаче CAS: ожидание 0x99 не совпадает, фактическое 0x0A вписывается обратно. (3) Ширина указателя обменивает и CAS-ит адреса статических буферов — сценарий публикации «на кого это указывает» (вершины стеков без блокировок, переключение экземпляров) — её настоящая работа. (4) Финальные IsLockFree + ThreadFence + SignalFence + Pause зажигают всех помощников. Программа вдвойне полезна как миграционная самопроверка: запустите её первой на новой платформе; пять верных строк — атомарный слой ведёт себя одинаково.

Третья полная программа: два жизненных цикла спин-блокировки

examples/concurrency/spin/main.c源码 ↗
#include <xrt.h>

#include <stdio.h>




















 */


/* 展示栈上短临界区锁的生命周期。 */
int main(void)
{
	xspinlock Spin;
	uint64 iCounter = 0u;

	if ( !xrtSpinInit(&Spin) ) {
		return 1;
	}
	if ( !xrtSpinLock(&Spin) ) {
		return 2;
	}
	iCounter++;
	if ( !xrtSpinUnlock(&Spin) || !xrtSpinUnit(&Spin) ) {
		return 3;
	}
	printf("counter=%llu\n", (unsigned long long)iCounter);

	/* 堆形态补集:Create / TryLock 持有与空闲正反 / Destroy。 */
	{
		xspinlock* pHeap = xrtSpinCreate();

		if ( (pHeap == NULL) ||
			!xrtSpinLock(pHeap) ||
			xrtSpinTryLock(pHeap) ||  /* 已持有:Try 必失败 */
			!xrtSpinUnlock(pHeap) ||
			!xrtSpinTryLock(pHeap) ||  /* 空闲:Try 必成功 */
			!xrtSpinUnlock(pHeap) ||
			!xrtSpinDestroy(pHeap) ) {
			return 4;
		}
	}
	printf("heap try=0/1 destroy ok\n");
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/concurrency/spin/main.c -lws2_32 -liphlpapicounter=1heap try=0/1 destroy ok

Что только что произошло. (1) Стековая форма проходит Init → Lock → 临界区(一条自增)→ Unlock → Unit (критическая секция: один инкремент) — секция ровно в одну инструкцию, законный диапазон вращения. (2) Кучная форма дополняет Create/TryLock/Destroy: TryLock обязан не удаться при удержании и удаться в свободном состоянии — оба состояния проверены. (3) Блокировка в примере защищает демонстрационную одиночную переменную — в реальном коде для голого счётчика ответ атомарный, не блокировка; пример запирает саму демонстрацию жизненного цикла. Глава 54 поставит спин-блокировку в трёхстороннее сравнение «спин против мьютекса против очереди без блокировок» с таймированием.

Четвёртая полная программа: остаток и истечение дедлайна

examples/concurrency/deadline/main.c源码 ↗
/* 相对毫秒等待和 double 秒计时。 */
#include <stdio.h>
#include <xrt.h>
int main(void)
{
    double Start = xrtTimer();
    xrtSleep(50);
    printf("elapsed_s=%.6f forever_ms=%lld\n", xrtTimer() - Start, (long long)XRT_WAIT_FOREVER);
    return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/concurrency/deadline/main.c -lws2_32 -liphlpapielapsed_s=0.050000 forever_ms=-1

xrtSleep(50) ожидает не менее 50 миллисекунд; разность показаний xrtTimer() измеряет прошедшее время в секундах.

9.4契约

  • Граница атомарности: атомарная операция гарантирует одну переменную, один шаг, без чередования; две атомарные операции вместе не имеют целостной атомарности — межпеременные инварианты принадлежат блокировкам (глава 53), передача владения — очередям (глава 21).
  • Семантика Fetch: всегда возвращать старое значение до операции; новое выводите сами — без рефлекторного «сходить ещё раз Load».
  • Перепись CAS: успех не трогает *pExpected; неудача вписывает фактически наблюдённое значение в *pExpected — цикл повторов правится от него, без дочитывания.
  • Законность порядка памяти: Load ∈ {RELAXED, ACQUIRE, SEQ_CST}; Store ∈ {RELAXED, RELEASE, SEQ_CST}; порядок неудачи CAS без RELEASE и не сильнее порядка успеха. Незаконные комбинации ставят XERR_ARGUMENT; объект не изменён.
  • Обещание нижнего предела порядка: реализации могут быть только сильнее документированного порядка, никогда слабее — пишите против документации, не против конкретной платформы.
  • Правило по умолчанию: SEQ_CST, пока не доказана возможность ослабления; деградация требует данных профилирования (глава 136) и письменного обоснования.
  • Жизненный цикл спин-блокировки: стек Init/Unit, статическая XRT_SPIN_INIT, куча Create/Destroy; уничтожение удерживаемой блокировки — неудача с XERR_STATE; TryLock неблокирующий, при удержании возвращает ложь.
  • Параметр ожидания: значение int64 в миллисекундах передаётся по значению и не владеет ресурсами; 0 означает одну попытку, XRT_WAIT_FOREVER — неограниченное ожидание, остальные отрицательные значения недопустимы.
  • xwaitresult, пять исключающих состояний: ERROR/OK/TIMEOUT/CANCELLED/CLOSED; тайм-аут и отмена — поток управления, не ошибки; они не входят в цепочку ошибок.
  • Правила потоков: атомарные объекты не имеют частично обновлённых состояний (любое наблюдение видит либо старое, либо новое); барьеры и Pause переносимы на все платформы.

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

Ловушка 1: публикация данных без порядка памяти (флаг солгал):Ловушка 1: публикация данных без порядка памяти (флаг солгал)

Симптом: увидев флаг «данные готовы», потребитель всё равно читает устаревшие данные; проблема появляется вероятностно и меняет частоту с CPU или флагами компилятора.

Причина: запись данных и запись флага обе использовали RELAXED (или вовсе обычные записи) — между ними нет обещания порядка; флаг может стать видимым раньше данных. Пара «публикация-получение» без одной стороны — пара, которой не существует.

错误写法
Data = Compute();                                   /* обычная запись */
xrtAtomic32Store(&Ready, 1u, XMEMORY_RELAXED);      /* слабая публикация: данных может ещё не быть видно */
正确写法
Data = Compute();                                   /* запись данных */
xrtAtomic32Store(&Ready, 1u, XMEMORY_RELEASE);      /* публикация: все предыдущие записи готовы */
/* пара на стороне потребителя: после xrtAtomic32Load(&Ready, XMEMORY_ACQUIRE) Data — опубликованная версия */
Ловушка 2: длинная работа внутри спин-блокировки:Ловушка 2: длинная работа внутри спин-блокировки

Симптом: под нагрузкой CPU насыщаются и пропускная способность падает; профилирование показывает горячую точку в цикле занятого ожидания xrtSpinLock.

Причина: критическая секция содержит IO, выделение памяти или длинное вычисление — вращающийся поток не работает, но отбирает ядра, держатель замедляется, ожидания растут, порочный круг. Вращение предполагает «секция в несколько инструкций».

错误写法
xrtSpinLock(&Lock);
SaveToDisk(&Record);        /* IO в критической секции: миллисекунды — вращающиеся сжигают целое ядро */
xrtSpinUnlock(&Lock);
正确写法
xrtSpinLock(&Lock);
iCount++;                   /* несколько инструкций: законный диапазон вращения */
xrtSpinUnlock(&Lock);
/* длинную работу — мьютексу (глава 53) — ждущий сон не жжёт CPU; или сперва вычислить, потом войти в короткую секцию для фиксации */
Ловушка 3: относительный тайм-аут внутри цикла повторов:Ловушка 3: относительный тайм-аут внутри цикла повторов

Симптом: цикл с надписью «повторять максимум десять секунд» реально работает двадцать с лишним; каждый круг сбрасывает бюджет ожидания, сумма расходится с числом кругов.

Причина: «сколько ждать каждый круг» приняли за «сколько ждать всего». Относительный тайм-аут ограничивает один круг; цикл умножает его на N; по-настоящему обещать система может абсолютную отсечку.

错误写法
for ( i = 0; i < 100; i++ ) {
	if ( TryOnce() ) { break; }
	Sleep(100000);        /* бюджет сбрасывается каждый круг: 100 кругов могут превысить 10 секунд */
}
正确写法
double D = xrtTimer() + 10.0;   /* сконструировать один раз, вне цикла */
while ( !TryOnce() ) {
	if ( (xrtTimer() >= D) ) { return XWAIT_TIMEOUT; }
	WaitOnce(((int64)ceil(fmax(0.0, D - xrtTimer()) * 1000.0)));                 /* остаток убывает, никогда не сбрасывается */
}
return XWAIT_OK;

Практические задания

Базовое: счёт без блокировок на четырёх потоках

Через xrtAtomic64FetchAdd (RELAXED) четыре потока прибавляют по миллиону; главный поток после объединения Load-ом сверяет сумму — ровно четыре миллиона. Критерий приёмки: сумма постоянна (одинакова при каждом запуске); замените атомарность на обычный ++ и пронаблюдайте потерянные обновления.

Продвинутое: изменение поля через CAS-цикл

Структура содержит Done и Value; через xrtAtomic32CompareExchange реализуйте «выставить Done в 1 и записать Value только если Done равен 0» — при неудаче правьтесь и повторяйте, используя перепись ожидаемого значения. Критерии приёмки: при многопоточной конкуренции успеха добивается ровно один поток; всюду без блокировок.

Вызов: спин против мьютекса — трёхстороннее таймирование

Одна задача счёта в трёх реализациях: атомарная, спин-блокировка и мьютекс (API главы 53); таймируйте каждую и сравните. Затем удлините критическую секцию (добавьте вычислений) и перемерьте; пронаблюдайте, где преимущество спина разворачивается. Критерий приёмки: квантованная точка перелома «в нескольких инструкциях побеждает спин, дальше мьютекс»; вывод перекрестно сверяется с разделом о разделении труда главы 53.

Итоги

ТемаКратко
Позицияфинал тома 1: атомарные примитивы + спин-блокировка + математика ожидания — общий фундамент конкурентности книги
Три шириныxatomic32/64/ptr; макросы Init (статика) и функции (время исполнения)
Семантика Fetchвозвращает старое значение; новое выводите сами — не возвращайтесь за Load
Перепись CASуспех не трогает ожидание; неудача переписывает фактическое — стартовая точка повтора
Порядок памятиRELAXED/ACQUIRE/RELEASE/ACQ_REL/SEQ_CST; по умолчанию SEQ_CST, деградация с обоснованием
Публикация-получениеRELEASE у пишущего, ACQUIRE у читающего; любая сторона RELAXED аннулирует обещание
ПомощникиThreadFence/SignalFence/Pause/IsLockFree
Спин-блокировкатолько секции в несколько инструкций; уничтожение удерживаемой → XERR_STATE; TryLock неблокирующий
deadlineабсолютная отсечка (монотонные миллисекунды); After один раз вне цикла, Remaining внутри
xwaitresultпять состояний ERROR/OK/TIMEOUT/CANCELLED/CLOSED; тайм-аут и отмена — поток управления
Разделение трудаодна переменная → атомарные; инварианты → блокировки; передача указателей → очереди (глава 21)

📖 Справочники API в этой главе:atomic (30 записей)spin (8 записей)wait (? записей)

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