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

Первая из трёх глав о корутинах разбирает слой примитивов: xrtCoCreate создаёт (со стеком — вернувшись, продолжаем с точки уступки), xrtCoResume доводит до следующей точки приостановки, xrtCoYield уступает; четыре состояния (READY/RUNNING/SUSPENDED/DONE) плюс три финала (RETURNED/CANCELLED/ERROR) описывают полный жизненный цикл; очистный стек (CleanupPush/Pop и Defer) гарантирует завершение в обратном порядке на любом пути выхода; корутина несёт собственный независимый слот ошибки и встроенную арену (глава 7). Бэкенд автоматически выбирает Windows Fiber / рукописное POSIX-переключение контекста (четыре архитектуры). Большинству бизнес-кода нужна средняя глава — планировщик, а не примитивы этой главы, — но примитивы суть фундамент понимания всех верхних слоёв.

55.1引入

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

Инженерный дивиденд модели — в IO-плотных сценах: в сетевой службе тысячи соединений каждое «ждёт данные» — поточная модель это либо взрыв потоков, либо усложнение select-опроса; модель корутин даёт каждому соединению по корутине, уступке в ожидании и восстановлению при приходе данных — пишешь код в синхронном стиле, получаешь асинхронную масштабируемость. Сетевой движок тома 7 использует её именно так.

55.2概念

Жизненный цикл: четыре состояния, три финала

1СозданиеxrtCoCreate(процедура, данные, конфигурация) -> READY (не запущена)
2ДоводкаxrtCoResume -> исполнение до Yield (SUSPENDED) или до конца (DONE)
3УступкаxrtCoYield - точка сотрудничества; следующий Resume продолжает с точки уступки
4ФиналыRETURNED (нормальный возврат) / CANCELLED (отмена) / ERROR (отказ процедуры)
5ЗавершениеDestroy успешен только для незапущенных или завершённых - активную сперва Cancel

Resume доводит до следующей точки приостановки — ключевая фраза для понимания корутин: один Resume продвигает корутину до следующего Yield или до конца — не «исполнить всё», а «исполнить до». Золотой стандарт (модель ошибок ch4) демонстрировал трёхтактную форму: после создания три Resume (после второго SUSPENDED, после третьего DONE+RETURNED). Сигнатура процедуры корутины ptr(ptr) — возвращаемое значение читается заимствованием через xrtCoResult.

Уступка и кооперативная отмена

xrtCoYield() возвращает xwaitresult — нормальная уступка даёт OK; при запросе отмены возвращается CANCELLED: корутина ощущает отмену в точке уступки, вычищается и возвращается (финал CANCELLED). xrtCoCancel только ставит отметку (та же кооперативная семантика, что у токена главы 54) — настоящее прекращение случается в следующей точке уступки самой корутины; xrtCoCurrent отвечает «внутри ли я корутины» (Yield из обычного кода не определён — сперва проверь, потом уступай). Семейство токенов отмены (глава 54) входит в мир корутин через планировщик — разворот в средней главе.

Очистный стек: гарантия на любом пути выхода

Ресурсы внутри корутины (открытые дескрипторы, заимствованные буферы) вверяются очистному стеку: xrtCoCleanupPush/Pop — ручное управление (узел на стеке, ноль накладных расходов) или xrtCoDefer — автоматическое; завершится ли корутина нормальным возвратом, отменой или ошибкой — очистный стек исполняется в обратном порядке (та же гарантия, что defer в Go). Это дополняет «урезание всего участка» арены главы 7: арена ведает памятью, очистный стек — действиями (незамещаемое памятью завершение вроде закрытия дескрипторов и отправки уведомлений).

Независимый слот ошибки и встроенная арена

Корутина переключает собственный слот ошибки (полный расцвет главы 4 в параллельной редакции): ошибка корутины A не загрязняет поток-носитель и другие корутины — миграция корутины не теряет контекста ошибок; внутри корутины xrtGetError/xrtTakeError работают с её собственным слотом, как обычно. Встроенная арена: корутина создаётся со своей temp-ареной, по завершении автоматический возврат — «запросная временная память» главы 7 в корутинной редакции (корутина и есть «запрос»).

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

Корутина жёстко принадлежит потоку, который её создал — Resume из другого потока не определён. Межпоточное сотрудничество идёт через Post планировщика (средняя глава) или передачу сообщений; эта дисциплина связана с реализацией контекста стековых корутин (TLS/защита стека) — не совет, а железное правило.

55.3示例

Полная программа: двухтактная корутина с уступкой

Из образца репозитория examples/concurrency/coroutine/main.c — 59 строк через создание/уступку/финалы:

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

#include <xrt.h>



/* 示例协程分两段完成工作。 */
static ptr exampleCoroutine(ptr pData)
{
	int* pValue = (int*)pData;

	*pValue += 1;
	if ( xrtCoYield() != XWAIT_OK ) {
		return NULL;
	}
	*pValue *= 2;
	return pValue;
}





















 */


/* 展示低级协程的创建、恢复和结果读取。 */
int main(void)
{
	int iValue = 20;
	xcoro* pCo = xrtCoCreate(exampleCoroutine, &iValue, NULL);

	if ( pCo == NULL ) {
		return 1;
	}
	(void)xrtCoResume(pCo);
	printf("after yield: %d\n", iValue);
	(void)xrtCoResume(pCo);
	printf("result: %d\n", *(int*)xrtCoResult(pCo));
	(void)xrtCoDestroy(pCo);
	(void)xrtCoThreadDetach();
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/concurrency/coroutine/main.c -lws2_32 -liphlpapiafter yield: 21result: 42

Что только что произошло. (1) Процедура корутины из двух частей: первая *pValue += 1 (20→21), Yield-уступка, вторая *pValue *= 2. (2) Первый Resume: исполнение до Yield и приостановка (SUSPENDED) — в этот момент печатается after yield: 21 (21 — плод первой части — точка уступки и есть точка наблюдения, здесь вызывающий видит промежуточное состояние). (3) Второй Resume: продолжение с точки уступки и завершение (DONE + RETURNED) — result: 42 это возвращаемое значение (21×2), прочитанное заимствованием через xrtCoResult. (4) Ритм трёх взаимодействий (Resume/Resume/чтение результата) — всё ощущение корутинных примитивов; планировщик (средняя глава) автоматизирует эти три такта.

Полная программа: зародыш планировщика

Из examples/concurrency/coroutine_scheduler/main.c — несколько корутин исполняются вперемешку:

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



/* 示例任务异步等待后打印编号。 */
static ptr exampleTask(ptr pData)
{
	int iTask = (int)(intptr_t)pData;

	(void)xrtCoSleep((uint64)iTask * 1000u);
	printf("task %d\n", iTask);
	return pData;
}



/* 调度器 post 在所属线程中创建真正的协程任务。 */
static void examplePost(xcosched* pSched, ptr pData)
{
	(void)xrtCoGo(pSched, exampleTask, pData, NULL);
}





















 */


/* 先投递多个轻量任务,再运行到投递和协程全部完成。 */
int main(void)
{
	xcosched* pSched = xrtCoSchedCreate();

	if ( pSched == NULL ) {
		return 1;
	}
	for ( int i = 1; i <= 3; i++ ) {
		if ( !xrtCoSchedPost(
			pSched,
			examplePost,
			(ptr)(intptr_t)i
		) ) {
			return 2;
		}
	}
	if ( !xrtCoSchedRun(pSched) ) {
		return 3;
	}
	(void)xrtCoSchedDestroy(pSched);
	(void)xrtCoThreadDetach();
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/concurrency/coroutine_scheduler/main.c -lws2_32 -liphlpapitask 1task 2task 3

Что только что произошло. (1) Три корутины поданы в планировщик — вывод task 1/2/3 по очереди демонстрирует перемешанное исполнение: не «одна до конца, потом другая», а каждая до своей точки приостановки с ротацией (точный ритм зависит от политики — разворот в средней главе). (2) Это минимальный образец «кооперативной многозадачности»: один поток, ноль блокировок, три «параллельные» задачи — конкурентность корутин не нуждается в многих ядрах, ей нужны точки чередования. (3) Планировщик оборачивает ручной ритм Create/Resume этой главы в автоматический ритм Post/Run — бизнес-код пишет «процедуру», планировщик ведает «доводкой». Образцы coroutine_tour и coroutine_event — расширенное чтение полного интерфейса и событий корутин.

55.4契约

  • Семантика доводки: Resume доводит до следующей точки приостановки (Yield) или финала; не до конца.
  • Три финала: RETURNED/CANCELLED/ERROR; Result/Error заимствованы — читать сразу после финала, использовать до Destroy.
  • Привязка к потоку: жёсткая принадлежность создавшему потоку; между потоками — Post планировщика или сообщения — железная дисциплина.
  • Кооперативная отмена: Cancel ставит отметку, Yield ощущает, после очистки финал CANCELLED; рекурсивный вход в себя возвращает XERR_STATE.
  • Очистный стек: обратный порядок на всех путях выхода; арена ведает памятью, очистный стек — действиями — взаимодополнение, не взаимозамена.
  • Независимый слот ошибки: ошибки корутины не загрязняют поток-носитель; встроенная арена возвращается вместе с корутиной.
  • Момент Destroy: успешен только для незапущенной или завершённой — активную сперва Cancel до финала.

Таблица корутина против потока

Корутина и поток — два полюса ящика параллельных инструментов; одна таблица задаёт меру. Цена: поток — стек 1 МБ плюс объект планирования ядра; корутина — десятки КБ (настраиваемо) плюс переключение в пользовательском режиме (наносекунды). Вытеснение: поток может быть вытеснен (решает планировщик); корутина уступает только по своей воле (решает код — потому «разорванной на середине исполнения» на уровне корутин не бывает). Параллелизм: потоки истинно параллельны (много ядер одновременно); корутины чередуются в одном потоке (параллелизм добывается комбинацией «много потоков × в каждом много корутин» — модель планировщика средней главы). Применяемость: CPU-плотный параллелизм — потоки (или пул задач); IO-плотная высокая конкурентность — корутины (много ждать, мало считать); чаще всего сочетание — по планировщику на поток, по сотне корутин на планировщик. Ментальная модель: потоки делят память и требуют синхронизации (глава 53); корутины чередуются в одном потоке — разделение данных между корутинами во многих сценах естественно взаимоисключающе (точка уступки и есть граница), но между экземплярами планировщика в разных потоках синхронизация по-прежнему нужна.

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

Обработка сетевых соединений (арена тома 7): по корутине на соединение — прямолинейная логика «читать до EOF» (цикл чтение-разбор-ответ), уступка в ожидании, восстановление при приходе данных; тысяча соединений = тысяча корутин + один-два потока — первая причина существования корутин. Оркестровка асинхронных потоков: многошаговый асинхронный процесс (проверить кеш, при промахе — базу, отложить, ответить) пишется прямой корутиной — каждый шаг await-уступка, после восстановления продолжаем; против цепочки Future (глава 58): у корутинной версии есть локальные переменные и стек — состояние не обязано явно жить в замыканиях. Генераторы и ленивые последовательности: поблочная обработка большого файла — каждый Yield выдаёт блок, потребитель задаёт темп (естественная форма противодавления). Общий критерий трёх хозяев: «ждём много, а логику хотим писать по прямой» — преимущество ощущения корутин в двух словах: «по прямой».

Со стеком и без: инженерный взгляд

Две школы реализации корутин заслуживают инженерного сопоставления. Со стеком (выбор XRT): у каждой корутины свой стек — уступка работает на любой глубине цепочки вызовов (Yield и внутри каркасного кода), локальные переменные сохраняются сами, отладчик видит настоящий стек; цена — память стека на корутину (от десятков КБ) и обмен стеков при переключении. Без стека (корутины C++20 / компиляция в конечный автомат): компилятор режет функцию на автомат — нулевая память стека, переключение почти бесплатно; цена — уступать может только тело самой корутинной функции (функции вниз по цепочке вызовов — нет), локальные переменные живут в автомате (время жизни ограничено), отладка показывает разобранную машину. XRT выбрал стек по причине: в C нет компиляторной магии, а переложить сложность рукописных автоматов на каждого пользователя недопустимо — «уступка на любой глубине + отладка по настоящему стеку» есть прагматичное решение для C-инженерии. Этот взгляд объясняет и настраиваемый размер стека корутины (то же соображение, что у стека потока главы 52).

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

Ловушка 1: Resume из другого потока:Ловушка 1: Resume из другого потока

Симптом: изредка крах или перепутывание состояния — корутину доводят в чужом потоке; воспроизведение зависит от тактов потоков.

Причина: корутина жёстко принадлежит создавшему потоку (реализация контекста привязывает TLS и защиту стека) — доводка через потоки есть неопределённое поведение.

错误写法
/* поток A создаёт */          /* поток B доводит */
xcoro* Co = xrtCoCreate(proc, Data, &Config);
xrtCoResume(Co);            /* B доводит корутину A - не определено */
正确写法
/* межпоточное сотрудничество через сообщения: корутина A доводится в потоке A */
/* поток B хочет дать толчок -> шлёт сообщение в очередь/канал A (главы 21/57) */
/* или через Post планировщика оставляет "обязанность доводки" правильному потоку (средняя глава) */
Ловушка 2: обратный порядок очистного стека нарушен:Ловушка 2: обратный порядок очистного стека нарушен

Симптом: порядок освобождения ресурсов перепутан — дескриптор закрыли, а его ещё используют; зависимое от порядка завершение изредка не срабатывает.

Причина: ручной CleanupPop выщелкнул не в очередности, или смешение Defer с ручным Pop увело порядок исполнения от обратного порядку вталкивания.

错误写法
xrtCoCleanupPush(&Node1, closeHandle, H1);   /* втолкнут первым: исполняться последним */
xrtCoCleanupPush(&Node2, releaseBuffer, B);  /* втолкнут вторым: исполняться первым */
xrtCoCleanupPop(&Node1);   /* вручную выщелкнули Node1 первым - обратный порядок сломан */
正确写法
xrtCoCleanupPush(&Node1, closeHandle, H1);
xrtCoCleanupPush(&Node2, releaseBuffer, B);
xrtCoCleanupPop(&Node2);   /* последним вошёл - первым вышел, порядок сохранён */
xrtCoCleanupPop(&Node1);
/* или всё через Defer: механизм корутины сам гарантирует порядок (рекомендуется - на одно измерение ошибки меньше) */

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

Базовое: генератор

Реализуйте генератор целочисленной последовательности на Resume/Yield: каждый Resume выдаёт одно значение (общая переменная или Result), после десятого DONE — корутинная примитивная редакция упражнения золотого стандарта.

Продвинутое: производитель-потребитель (однопоточная редакция)

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

Вызов: отменяемая корутина-конвейер

Трёхступенчатый корутинный конвейер (чтение/вычисление/запись) с ресурсами ступеней на очистном стеке; после попадания токена отмены все три ступени чисто завершаются (финал CANCELLED, очистный стек исполнен целиком, арена возвращена). Критерии приёмки: отмена в любой момент — ноль утечек ресурсов (статистика главы 6); финалы и порядок очистки аудируемы структурным журналом.

Итоги

ТемаКратко
Трёхтактный ритмCreate (READY) → Resume×N (до Yield или DONE) → чтение Result в финале
Четыре состояния, три финалаREADY/RUNNING/SUSPENDED/DONE; RETURNED/CANCELLED/ERROR
Семантика Yieldпродолжение с точки уступки; возврат CANCELLED означает попадание отмены - почистить и вернуться
Очистный стекPush/Pop вручную или Defer автоматически; обратный порядок покрывает все пути выхода
Независимые средстваслот ошибки изолирован (миграция не загрязняет) + встроенная арена (возвращается с корутиной)
Привязка к потокужёсткая принадлежность создавшему потоку - между потоками Post планировщика или сообщения (железно)
Порог Destroyуспешен только незапущенной или завершённой - активную сперва Cancel до финала

📖 Справочники API в этой главе:coroutine (58 записей)

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