Ориентация главы
Первая из трёх глав о корутинах разбирает слой примитивов: 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概念
Жизненный цикл: четыре состояния, три финала
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 строк через создание/уступку/финалы:
#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; }
Что только что произошло. (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 — несколько корутин исполняются вперемешку:
#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; }
Что только что произошло. (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).
Типичные ошибки
Симптом: изредка крах или перепутывание состояния — корутину доводят в чужом потоке; воспроизведение зависит от тактов потоков.
Причина: корутина жёстко принадлежит создавшему потоку (реализация контекста привязывает TLS и защиту стека) — доводка через потоки есть неопределённое поведение.
/* поток A создаёт */ /* поток B доводит */ xcoro* Co = xrtCoCreate(proc, Data, &Config); xrtCoResume(Co); /* B доводит корутину A - не определено */
/* межпоточное сотрудничество через сообщения: корутина A доводится в потоке A */ /* поток B хочет дать толчок -> шлёт сообщение в очередь/канал A (главы 21/57) */ /* или через Post планировщика оставляет "обязанность доводки" правильному потоку (средняя глава) */
Симптом: порядок освобождения ресурсов перепутан — дескриптор закрыли, а его ещё используют; зависимое от порядка завершение изредка не срабатывает.
Причина: ручной 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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки