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

Проблема, решаемая отменой: как заставить «ожидающую/исполняющуюся» работу остановиться. Ответ XRT — токен отмены (xcancel): xrtCancelRequest инициирует отмену (true только в первый раз, с возбуждением слушателя), xrtCancelRequested запрашивает, не был ли поражён токен или любой из предков, xrtCancelWatch регистрирует наблюдателя (пробуждение обратным вызовом — синхронно не более одного раза, xrtCancelTriggered опрашивает попадание слушателя); xrtCancelChild порождает дочерний токен, образуя дерево отмены (отмена родителя распространяется на всех потомков); xrtCancelRef ведёт подсчёт ссылок, позволяя многим держателям делить один токен. Глава позиционирует себя как «главу системы отмены»: токен — фундамент, а интерфейсы отмены Channel/Future/групп задач в главах 56~59 всё стоят на нём.

54.1引入

Служба получила сигнал остановки (глава 49), главный цикл замер — но пять рабочих потоков ещё мёртво ждут на своих приёмах Channel, три Future не завершены, два соединения ещё заканчивают обмен. Без системы отмены решения: тайм-аутный опрос у каждого ожидания (высокая задержка), глобальный флаг с проверками повсюду (инвазивная переписка каждой точки ожидания), или просто exit всего процесса (вся работа в полёте потеряна). Все три вида приходилось видеть; все три болезненны.

Решение токена отмены: создаётся корневой токен, подвешенный на сигнал остановки; каждая работа держит его (или его дочерний); отменяемое ожидание (например xrtChannelRecvCancel) сливает «ждать данные» и «ждать отмену» в одно ожидание — кто раньше, тот и выиграл. Сигнал остановки поражает корень → распространение по всему дереву → все мёртвые ожидания пробуждаются одновременно с XWAIT_CANCELLED → каждое идёт своим путём завершения. Один запрос — всё дерево просыпается — каждое завершается само — это всё обещание системы отмены.

54.2概念

Тройка токена: запрос, наблюдение, распространение

1СозданиеxrtCancelCreate() -> токен; Ref/Destroy делят его счётом ссылок
2ЗапросxrtCancelRequest(токен) -> отметка о поражении (true только первый раз; однонаправленно, необратимо)
3НаблюдениеRequested опрашивает токен/цепочку предков / обратный вызов Watch (не более одного раза, синхронно) + Triggered опрашивает слушателя
4РаспространениеCancelChild(родитель) -> дочерний токен - родитель поражён, потомки автоматически; потомок на родителя не влияет

Три проектных пункта. Отмена однонаправленна и необратима — запрос вступает в силу немедленно, «отмены отмены» нет; это упрощение покупает безблокировочный быстрый путь (проверка Triggered — одно атомарное чтение). Запрос и наблюдение разделены — Request только ставит отметку; кто на каком ожидании будет пробуждён, зависит от того, кто зарегистрировал наблюдение; код, инициирующий отмену, и код, отвечающий на неё, могут совершенно не знать друг о друге. Слушатель однократный — обратный вызов Watch исполняется синхронно не более одного раза (внутри — отметка/пробуждение; дальнейшие проверки опрашивают Triggered) — обратный вызов не докучает повторами и не требует защиты от реентерабельности.

Дерево отмены: структурное распространение

xrtCancelChild(父) (родитель) порождает дочерний токен: родитель поражён → поражены все потомки (направление распространения одно); потомок отменён → родитель и братья не затронуты (локальный отказ). Форма дерева соответствует форме вызовов: обработка одного запроса порождает свой дочерний токен — тайм-аут этого запроса отменяет работу только этого запроса (локально); остановка службы отменяет корень (глобально). «Отмена по области видимости» структурного параллелизма главы 60 — регламентированное употребление именно этого дерева: вошли в область — токен родился; вышли из области — поддерево полностью собрано.

Отменяемое ожидание: единая поза

Ожидающие интерфейсы параллельной системы XRT следуют одной форме: объект ожидания + опциональный токен. xrtChannelRecvCancel(通道, ..., 令牌) (канал, ..., токен) ждёт данные или отмену; семейство ожиданий Future принимает токен так же; то же верно для сводящих ожиданий групп задач. Возвращаемый xwaitresult единообразно сообщает исход ожидания — XWAIT_OK (дождались), XWAIT_CANCELLED (поражение отменой), XWAIT_TIMEOUT (тайм-аут). Один перечислитель кодов возврата проходит через все ожидания — семя, посеянное главой 41, всходит по всей параллельной системе. Своё отменяемое ожидание (пользовательская блокирующая структура, стыкуемая с токеном) пишется через Wake-подобный обратный вызов Watch — stopWork образца cancel и есть шаблон формы.

Семантика финальных состояний: отмена не отказ

XWAIT_CANCELLED и ошибка (глава 4) — два измерения: отмена это решение потока управления («мы решили не делать»), ошибка это **провал операции» («делали — не вышло»). Цена путаницы: отмена уходит в отчёт как ошибка (мониторинг даёт ложные срабатывания доли отказов), ошибка проглатывается как отмена (настоящий провал замолчан). Группы задач главы 60 сольют их в полную классификацию конечных состояний (успех/отказ/отмена); эта глава сперва утверждает идею «отмена ≠ отказ».

54.3示例

Полная программа: распространение и наблюдатели

Из образца репозитория examples/concurrency/cancel/main.c:

examples/concurrency/cancel/main.c源码 ↗
#include <stdio.h>

#include <xrt.h>



/* 应用取消回调只负责唤醒或标记对应工作。 */
static void stopWork(ptr pData)
{
	bool* pStopped = (bool*)pData;

	*pStopped = true;
}
























 */


/* 用父令牌一次取消一组子操作。 */
int main(void)
{
	xcancel* pRequest = xrtCancelCreate();
	xcancel* pOperation;
	xcancelwatch* pWatch;
	bool bStopped = false;

	if ( pRequest == NULL ) {
		return 1;
	}
	pOperation = xrtCancelChild(pRequest);
	if ( pOperation == NULL ) {
		xrtCancelDestroy(pRequest);
		return 1;
	}
	pWatch = xrtCancelWatch(pOperation, stopWork, &bStopped);
	if ( pWatch == NULL ) {
		xrtCancelDestroy(pOperation);
		xrtCancelDestroy(pRequest);
		return 1;
	}

	/* Ref:增加令牌引用(多持有者共享同一取消源),用完各Destroy一次。 */
	xcancel* pExtra = xrtCancelRef(pOperation);

	(void)xrtCancelRequest(pRequest);
	printf("operation stopped: %s\n", bStopped ? "yes" : "no");
	printf("watch-triggered=%d\n", xrtCancelTriggered(pWatch) ? 1 : 0);
	xrtCancelDestroy(pExtra);

	xrtCancelUnwatch(pWatch);
	xrtCancelDestroy(pOperation);
	xrtCancelDestroy(pRequest);
	return bStopped ? 0 : 1;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/concurrency/cancel/main.c -lws2_32 -liphlpapioperation stopped: yeswatch-triggered=1

Что только что произошло. (1) xrtCancelCreate строит токен, xrtCancelChild(父) (родитель) порождает дочерний — наблюдающая сторона опрашивает Requested по цепочке предков (на дочернем токене спрашивается «отменён ли ты или кто-то из твоих предков»), распространение не требует явных уведомлений. (2) Рабочая сторона совмещает две позы наблюдения: опрос Requested (вердикт operation stopped: yes — с цепочкой предков) и регистрация обратного вызова xrtCancelWatch (stopWork ставит флаг остановки — обратный вызов наблюдателя только пробуждает или отмечает, в согласии с дисциплиной «лёгких обратных вызовов» тома; обратный вызов не более одного раза, затем опрос Triggered принимает эстафету); watch-triggered=1 утверждает попадание слушателя. (3) Балансировка ссылок xrtCancelRef/Destroy — многим держателям, делящим токен, каждому свой Ref и свой Destroy. (4) Путь остановки пройден: Request → распространение → попадание наблюдателя → завершение работы — четырёхтактный ритм системы отмены.

Полная программа: отменяемый приём из канала

Из examples/concurrency/channel_cancel/main.c — стандартное лекарство от мёртвого ожидания на канале:

examples/concurrency/channel_cancel/main.c源码 ↗
#include <stdio.h>

#include <xrt.h>


















 */


/* 演示用一个取消令牌中断无限 Channel 接收。 */
int main(void)
{
	xchannel tChannel;
	xcancel* pCancel;
	ptr pItem = NULL;
	xwaitresult iResult;

	if ( !xrtChannelInit(&tChannel, 1u) ) {
		return 1;
	}
	pCancel = xrtCancelCreate();
	if ( pCancel == NULL ) {
		(void)xrtChannelUnit(&tChannel);
		return 2;
	}
	if ( !xrtCancelRequest(pCancel) ) {
		xrtCancelDestroy(pCancel);
		(void)xrtChannelUnit(&tChannel);
		return 3;
	}

	iResult = xrtChannelRecvCancel(&tChannel, &pItem, pCancel);
	printf("cancelled: %s\n", iResult == XWAIT_CANCELLED ? "yes" : "no");

	xrtCancelDestroy(pCancel);
	if ( !xrtChannelUnit(&tChannel) ) {
		return 4;
	}
	return iResult == XWAIT_CANCELLED ? 0 : 5;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/concurrency/channel_cancel/main.c -lws2_32 -liphlpapicancelled: yes

Что только что произошло. (1) Принимающий поток ждёт на xrtChannelRecvCancel — одно ожидание следит сразу за двумя событиями (данные пришли / отмена поразила), кто раньше — тот и победил; это не опрос, а мультиплексное ожидание уровня ядра. (2) После того как главный поток делает Request, приём немедленно возвращается с XWAIT_CANCELLED — источник вердикта cancelled: yes; риска потери данных нет (отмена первой — значит, данных и не дождались). (3) Контраст со «схемой тайм-аутного опроса»: RecvCancel без задержки (отмена будит мгновенно), без холостого вращения (CPU не жжёт), без инвазии (правка в одной точке ожидания). Образец deadline — родственная глава той же идеи: relative milliseconds/Expired в сочетании с токеном отмены даёт трёхстороннее ожидание «данные, или отмена, или тайм-аут» (стандартная полная форма параллельного ожидания).

54.4契约

  • Однонаправленность и необратимость: Request вступает в силу немедленно (true только первый раз), отмены отмены нет; Requested включает цепочку предков — распространение и есть запрос.
  • Направление распространения: родитель→потомок однонаправленно; отмена потомка не трогает родителя и братьев — семантическая основа локального отказа.
  • Лёгкое наблюдение: обратный вызов Watch синхронно не более одного раза и только пробуждает/отмечает; далее опрос Triggered — тяжёлая работа завершается на ожидающей стороне.
  • Единое ожидание: интерфейсы семейства = объект ожидания + опциональный токен; исход через xwaitresult (OK/CANCELLED/TIMEOUT).
  • Отмена ≠ отказ: решение потока управления и провал операции — два измерения; отчёт и статистика раздельно (финальная классификация главы 60).
  • Балансировка ссылок: многим держателям — каждому Ref/Destroy; дерево токенов закрывается с жизненными циклами держателей; утёкший токен — та же утечка, что и память.

От примеров к инженерии: три хозяина отмены

Остановка службы (глава 49 о сигналах, вторая половина): корневой токен подвешен на сигнал остановки → все ожидания рабочих потоков переводятся в отменяемую форму (RecvCancel/Future с токеном) → сигнал делает Request корню → всё дерево просыпается → каждое завершается → главный поток сводит всех (Wait по потокам) и выходит. Пяти секундный лимит «ожидания в полёте» (конвенция главы 49) в системе токенов получается сам: после Request — сводящий WaitFor с deadline, по превышению принудительный выход.Тайм-аут запроса: обработка каждого запроса порождает дочерний токен (CancelChild(根) (корень)), подвешенный на дедлайне запроса — тайм-аут одного запроса отменяет только его рабочее поддерево, корень и братья не замечают; AcceptWait с дедлайном у TCP главы 65 — сетевая редакция того же.Пользовательская отмена: Ctrl+C в CLI, кнопка в интерфейсе — обратный вызов Watch переводит событие UI в Request; вся реализация «кнопки отмены» — одна строка Request.

Дерево отмены и области видимости: репетиция структурного параллелизма

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

Взгляд на дизайн: «экономика контрольных точек» системы отмены

Оперативность отмены зависит от плотности контрольных точек — а плотность имеет цену (каждый Requested — атомарное чтение, дёшево, но накапливается; логика, порезанная контрольными точками, теряет читабельность). Экономическое балансирование: ярусная проверка — грубые межсегментные проверки для миллисекундной отзывчивости, шаговые проверки на критических путях (в циклах), чисто вычислительные куски прерываются Watch асинхронно (где платформа позволяет) или сегментной самопроверкой; плотность выбирает вызывающий — библиотечные функции дают отменяемые версии (форма RecvCancel), а не проверяют тайком внутри — вызывающий знает бюджет задержки, библиотека нет; проверка измерением — задержка отмены (от Request до фактической остановки работы) как наблюдаемый показатель идёт в журнал (поля главы 50), дрейф виден сразу. Три приёма вместе превращают «как быстро мы останавливаемся» из мистики в проектируемое, измеряемое и регрессируемое инженерное свойство.

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

Ловушка 1: проверки отмены только в заголовке цикла:Ловушка 1: проверки отмены только в заголовке цикла

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

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

错误写法
while ( running ) {
	ProcessOneBigChunk();   /* большой отрезок на 30 секунд - отмене его ждать */
	if ( xrtCancelRequested(pToken) ) break;   /* контрольная точка только в заголовке */
}
正确写法
while ( running ) {
	for ( int i = 0; i < ChunkSteps; ++i ) {
		ProcessStep(i);
		if ( xrtCancelRequested(pToken) ) {   /* контрольная точка на уровне шагов */
			goto Cleanup;
		}
	}
}
Cleanup:
	ReleasePartialWork();   /* завершение частичной работы */
Ловушка 2: отмена уходит в отчёт как ошибка:Ловушка 2: отмена уходит в отчёт как ошибка

Симптом: каждый релиз (штатная отмена пачки работ в полёте) даёт монитору «всплеск доли ошибок»; откаты и аварии смешаны в одном канале тревог.

Причина: XWAIT_CANCELLED делит путь отчёта с ошибкой — событие потока управления посчитано аварией.

错误写法
if ( xrtChannelRecvCancel(&Ch, ..., pToken) != XWAIT_OK ) {
	ReportError("recv failed");   /* и отмена попадает в ошибки - ложная тревога */
}
正确写法
xwaitresult R = xrtChannelRecvCancel(&Ch, ..., pToken);
if ( R == XWAIT_OK ) { Handle(item); }
else if ( R == XWAIT_CANCELLED ) { LogInfo("recv cancelled"); /* поток управления */ }
else { ReportError("recv failed"); /* настоящий провал */ }

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

Базовое: проверка дерева распространения

Постройте трёхуровневое дерево токенов (корень→средний→лист); сделайте Request корню и утвердите Requested на всех трёх уровнях; затем Request среднему — и утвердите попадание только среднего и листа, корень не затронут — обе стороны направления распространения проверены по разу.

Продвинутое: трёхстороннее ожидание

Данные, отмена, тайм-аут в трёхстороннем слиянии: RecvCancel с дедлайном (или комбинация Watch+DeadlineAfter) — все три исхода вызваны по разу, коды возврата утверждены.

Вызов: останавливаемая пакетная обработка

Длинная пакетная задача: контрольные точки уровня шагов + сохранение частичного прогресса (запись точки прерывания) + возобновление с контрольной точки после отмены. Критерии приёмки: отмена в любой момент останавливает работу в пределах 10 секунд; прогресс не теряется (повтор пропускает завершённые сегменты); причина отмены (какой токен, почему) идёт в структурный журнал.

Итоги

ТемаКратко
ТройкаRequest (true только первый раз) / Requested опрос (с предками) / Watch слушатель (не более одного раза)
РаспространениеCancelChild родитель→потомок однонаправленно; отмена потомка — локальный отказ
СсылкиRef/Destroy в равновесии между держателями - та же дисциплина, что у деревьев значений
Единое ожиданиеобъект ожидания + опциональный токен → xwaitresult (OK/CANCELLED/TIMEOUT)
Контрольные точкикооперативные - шаговые проверки Requested; точки встроены в длинные операции
Семантикаотмена = решение потока управления ≠ ошибка = провал операции - отчёт раздельно
ПричинаRequest несёт причину → наблюдатель может прочесть - диагностические поля распространяются с деревом

📖 Справочники API в этой главе:cancel (18 записей)channel (40 записей)

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