Ориентация главы
Этой главой том 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 — это не совет по очистке, а принудительная проверка от утечек.
Примитивы ожидания: математика дедлайнов и пятирезультатный исход
Интерфейсы ожидания принимают относительный интервал в миллисекундах типа int64; XRT_WAIT_FOREVER означает неограниченное ожидание. Разность показаний xrtTimer() типа double позволяет рассчитать оставшееся время общего бюджета.
xwaitresult раскладывает «исход ожидания» на пять взаимоисключающих значений: XWAIT_ERROR (настоящая неудача), XWAIT_OK (успех), XWAIT_TIMEOUT (время вышло), XWAIT_CANCELLED (отменено — токены отмены главы 54 кончаются здесь), XWAIT_CLOSED (ожидаемый объект закрыт). Замысел: отделить нормальный поток управления от ошибок — тайм-аут и отмена ожидаемы ветви, которые обрабатывает вызывающий, а не «исключения», запихнутые в цепочку ошибок. От xrtThreadWait главы 53 до ожиданий соединений сетевой библиотеки возвращается именно это перечисление — выучите семантику здесь; систематизированное ожидание и отмена разворачиваются в томе 6.
9.3示例
Первая полная программа: атомарный счёт и однозначный CAS
*/ #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; }
Что только что произошло. (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
*/ #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; }
Что только что произошло. (1) Все три ширины проходят Exchange и побитовое семейство — «Fetch возвращает старое» — прямое доказательство в каждом утверждении (FetchAnd возвращает 0x3C, а новое 0x0C — его И с маской). (2) Вторая строка вывода посвящена переписи при неудаче CAS: ожидание 0x99 не совпадает, фактическое 0x0A вписывается обратно. (3) Ширина указателя обменивает и CAS-ит адреса статических буферов — сценарий публикации «на кого это указывает» (вершины стеков без блокировок, переключение экземпляров) — её настоящая работа. (4) Финальные IsLockFree + ThreadFence + SignalFence + Pause зажигают всех помощников. Программа вдвойне полезна как миграционная самопроверка: запустите её первой на новой платформе; пять верных строк — атомарный слой ведёт себя одинаково.
Третья полная программа: два жизненных цикла спин-блокировки
#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; }
Что только что произошло. (1) Стековая форма проходит Init → Lock → 临界区(一条自增)→ Unlock → Unit (критическая секция: один инкремент) — секция ровно в одну инструкцию, законный диапазон вращения. (2) Кучная форма дополняет Create/TryLock/Destroy: TryLock обязан не удаться при удержании и удаться в свободном состоянии — оба состояния проверены. (3) Блокировка в примере защищает демонстрационную одиночную переменную — в реальном коде для голого счётчика ответ атомарный, не блокировка; пример запирает саму демонстрацию жизненного цикла. Глава 54 поставит спин-блокировку в трёхстороннее сравнение «спин против мьютекса против очереди без блокировок» с таймированием.
Четвёртая полная программа: остаток и истечение дедлайна
/* 相对毫秒等待和 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; }
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 переносимы на все платформы.
Типичные ошибки
Симптом: увидев флаг «данные готовы», потребитель всё равно читает устаревшие данные; проблема появляется вероятностно и меняет частоту с CPU или флагами компилятора.
Причина: запись данных и запись флага обе использовали RELAXED (или вовсе обычные записи) — между ними нет обещания порядка; флаг может стать видимым раньше данных. Пара «публикация-получение» без одной стороны — пара, которой не существует.
Data = Compute(); /* обычная запись */ xrtAtomic32Store(&Ready, 1u, XMEMORY_RELAXED); /* слабая публикация: данных может ещё не быть видно */
Data = Compute(); /* запись данных */ xrtAtomic32Store(&Ready, 1u, XMEMORY_RELEASE); /* публикация: все предыдущие записи готовы */ /* пара на стороне потребителя: после xrtAtomic32Load(&Ready, XMEMORY_ACQUIRE) Data — опубликованная версия */
Симптом: под нагрузкой CPU насыщаются и пропускная способность падает; профилирование показывает горячую точку в цикле занятого ожидания xrtSpinLock.
Причина: критическая секция содержит IO, выделение памяти или длинное вычисление — вращающийся поток не работает, но отбирает ядра, держатель замедляется, ожидания растут, порочный круг. Вращение предполагает «секция в несколько инструкций».
xrtSpinLock(&Lock); SaveToDisk(&Record); /* IO в критической секции: миллисекунды — вращающиеся сжигают целое ядро */ xrtSpinUnlock(&Lock);
xrtSpinLock(&Lock); iCount++; /* несколько инструкций: законный диапазон вращения */ xrtSpinUnlock(&Lock); /* длинную работу — мьютексу (глава 53) — ждущий сон не жжёт CPU; или сперва вычислить, потом войти в короткую секцию для фиксации */
Симптом: цикл с надписью «повторять максимум десять секунд» реально работает двадцать с лишним; каждый круг сбрасывает бюджет ожидания, сумма расходится с числом кругов.
Причина: «сколько ждать каждый круг» приняли за «сколько ждать всего». Относительный тайм-аут ограничивает один круг; цикл умножает его на 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-ом сверяет сумму — ровно четыре миллиона. Критерий приёмки: сумма постоянна (одинакова при каждом запуске); замените атомарность на обычный ++ и пронаблюдайте потерянные обновления.
Структура содержит 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 (? записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки