- Корутините обобщават подпрограмите, като запазват локалното състояние и възобновяват изпълнението си в точки на прекъсване, което позволява естествено изразяване на машини на състоянията, генератори и кооперативна паралелност.
- C реализациите са еволюирали от ръчно манипулиране на стека и POSIX контекстни API до макро-базирани апроксимации и преносими библиотеки с корутини, изградени върху превключване на контекст на потребителско ниво.
- C++20 стандартизира модел на безстекова корутина с обещания,
co_await,co_yieldи рамки на корутини, позволявайки на библиотеките да дефинират асинхронни и генераторни абстракции на високо ниво. - Стандартизираният модел, комбиниран с чакащи и персонализирани типове обещания, унифицира използването на корутини в библиотеките, като същевременно поддържа предвидима производителност и контрол.
Корутините се намират в завладяваща средна позиция между класическите функции и пълноценните нишки, и тяхната история от ниско ниво на C трикове до стандартизирана поддръжка на езика C++20 е една от най-интересните еволюции в съвременното системно програмиране. Ако някога сте се опитвали да жонглирате с обратни извиквания, машини на състоянията и синхронизация на нишки, само за да обработвате неблокиращ входно/изходен трафик, вече сте се сблъсквали с вида болка, която корутините са предназначени да облекчат.
В тази статия ще разгледаме как сопрограмите са еволюирали от ръчно изработени C хакове и POSIX контекстни API до високо ниво, безстеков модел на C++20 сопрограми. обяснение какво всъщност представлява корутината, как се различава от генераторите, нишките и влакната, какво означава „stackful“ срещу „stackless“ и как работи машинарията на C++20 (обекти на promise, обработчици на корутини, co_await, co_yield, co_return) всъщност се държи „под капака“.
Какво всъщност е корутина?
Няма единно универсално прието формално определение за корутина, но литературата се сближава относно две ключови свойства, които отличават сопрограмите от обикновените подпрограми:
- Местната държава оцелява въпреки суспензиите: Локалните за сопрограмата данни се запазват между активациите, така че всеки екземпляр на сопрограма се държи като обект с памет.
- Изпълнението може да бъде спряно и по-късно възобновено от същата точка: Когато контролът напусне корутина, той може да бъде въведен отново в точната точка на спиране, вместо винаги да се започва отгоре, както е при нормална функция.
Вместо да имат еднократно влизане и излизане, подобно на подпрограмите, сопрограмите поддържат множество точки на влизане и излизане през целия си жизнен цикъл, което ги прави мощни за изразяване на производители, потребители, машини на състоянията, кооперативни планировчици и асинхронни работни потоци в линеен, четим стил.
Основни измерения на дизайна на корутини
Системите от корутини в реалния свят се различават по три важни оси, които определят как се държат и колко изразителни са: моделът на прехвърляне на контрол, дали сопрограмите са първокласни стойности и дали са подредени или неподредени.
Първо, механизмът за трансфер на управление разделя асиметричните от симетричните корутини. В асиметричен дизайн, активната корутина може да се върне само към директния си извиквател (използвайки операция, концептуално подобна на yield), а повикващият го възобновява по-късно (с операция, подобна на resume). В симетрични дизайни, една сопрограма може изрично да прехвърли контрола на всяка друга сопрограма, вместо винаги да се връща към този, който я е извикал.
Второ, някои езици третират екземплярите на сопрограми като първокласни обекти, които можете да съхранявате, прехвърляте и манипулирате свободно, докато други представят сопрограмите само като синтактични конструкции с ограничени начини за взаимодействие с тях. Първокласната поддръжка драстично увеличава гъвкавостта и композируемостта.
Трето, сопрограмите могат да бъдат подредени (stackful) или неподредени (nestackless). Една подредена сопрограма може да се преустанови дълбоко във вложен стек от извиквания; когато се възобнови, всеки кадър в този стек продължава оттам, където е спрял. Безподредените сопрограми се преустановяват само на нивото на самата функция на сопрограмата: обикновените помощни функции не могат да се справят, освен ако самите те не са сопрограми или не са специално анотирани.
Терминът „пълна корутина“ е предложен за най-изразителната комбинация: подредена, първокласна корутина, симетрична или асиметрична, което е достатъчно мощно, за да изрази еднократни продължения или разграничени продължения. Въпреки че симетричните и асиметричните стилове имат еквивалентна изразителна сила, асиметричният модел често се усеща по-„рутинен“ и познат на повечето програмисти.
Подпрограми, корутини, генератори и нишки
Подпрограмите могат да се разглеждат като специален случай на сопрограми със силно ограничен поток на управление и поведение на състоянията. Нормалната функция винаги започва от първата си инструкция, излиза веднъж и след това изхвърля локалното си състояние. За разлика от това, корутината може да прехвърли контрола на други корутини, да бъде възобновена по-късно в точката на изход и да запази състоянието си по време на тези прехвърляния. Множество екземпляри на корутини на една и съща функция могат да съществуват едновременно, всеки със свои собствени запазени локални данни.
Генераторите образуват забележително подмножество от корутини, понякога наричани „полукорутини“. Подобно на корутините, генераторите могат да преустановят изпълнението си многократно и по-късно да го продължат, но винаги се връщат към директния си извиквател и нямат начин да пренасочат изпълнението към произволна трета корутина. Това ограничение е умишлено: генераторите са оптимизирани за имплементиране на итератори и лениви последователности, където всеки yield просто означава „да се създаде стойност за този, който ме повтаря“.
Всъщност, можете да емулирате общи корутини, като наслоите диспечер върху генераторна система, например чрез наличието на трамплин от най-високо ниво, който получава токени от генератори и решава кой генератор да активира следващия. Тази техника е била използвана исторически в езици като ранните версии на Python, които са имали само генератори, но не и вградени примитиви на корутини.
Корутините често се сравняват с нишки, но те са по същество за кооперативно планиране, а не за превантивен паралелизъм. Корутините осигуряват паралелизъм в смисъл на преплитане на задачи, без да променят общата семантика, но те не се изпълняват едновременно на множество ядра сами по себе си. Корутината дава контрол само в изрични точки на спиране, така че кодът между тези точки се изпълнява без прекъсване от други корутини.
Този кооперативен модел елиминира много от главоболията при синхронизация, често срещани при нишките: Тъй като само една корутина се изпълнява едновременно в даден планировчик, често не са ви необходими мютекси или атомни операции за обикновено споделено състояние. От друга страна, самите корутини няма да експлоатират множество ядра на процесора, освен ако не ги комбинирате с нишки или многонишков изпълнител.
Класически пример за корутина: производител-потребител
Учебникарска демонстрация на симетрични сопрограми е моделът производител-потребител със споделена опашка. Една сопрограма генерира елементи и ги поставя в опашка, докато тя се запълни, след което отстъпва на потребителя; потребителят изважда елементи, докато опашката се изпразни, след което отстъпва обратно на производителя. Изпълнението се променя, като всяка страна съвместно отстъпва контрол.
В такава имплементация, производителят и потребителът изглежда работят „паралелно“ от гледна точка на програмиста, въпреки че всъщност те просто прескачат напред-назад в рамките на една нишка на изпълнение. Няма нужда от нишки на ниво операционна система или превключване на контекста: операцията yield може да бъде скок на ниско ниво, който пренастройва активния стеков кадър.
Този пример често се използва за въвеждане на многонишковост, но е важно да се отбележи, че самите корутини са достатъчни, за да изразят логиката, и че замяната им с нишки може да е ненужна или дори вредна в среди, които се интересуват от гаранции в реално време или минимални разходи по време на изпълнение.
Защо корутините са важни: машини на състоянията, актьори и асинхронни работни процеси
Тъй като сопрограмите запазват както точката си на изпълнение, така и локалните си променливи в различните добиви, Те предоставят много естествен начин за имплементиране на сложни машини на състоянията, без разпръснати оператори за превключване, флагове или явни програмни броячи. Текущата точка на спиране буквално представлява текущото състояние.
Корутините също са подходящи за модели на паралелизъм в стил актьор, като тези, използвани в много игрови енджини. Всеки актьор може да бъде реализиран като корутина, която периодично връща контрола на централен планировчик, който изпълнява един актьор след друг в една нишка. Тази кооперативна многозадачност елиминира необходимостта от по-голямата част от заключването, като същевременно осигурява адаптивно поведение.
Генераторите, изградени върху корутини, са идеални за работа с потоци и преминаване през структури от данни. особено когато искате мързеливо, при поискване производство на стойности. Вместо да въвежда стойности в потребител, генераторът позволява на потребителя да извлича стойности една по една, използвайки прост цикъл.
Корутините също така блестят в комуникационни модели като тръбопроводи и комуникация на последователни процеси (CSP), където всеки етап е сопрограма, която се изпълнява, когато чака вход или изход. След това планировчикът възобновява сопрограмите, когато комуникационните им канали са готови, предоставяйки елегантна алтернатива на циклите на събития с тежки обратни извиквания.
И накрая, много числени библиотеки използват стил, понякога наричан „обратна комуникация“, където решаващият метод се самозадържа винаги, когато се нуждае от потребителя да предостави някаква функция, след което възобновява работата си, след като потребителят отговори. Корутините предоставят директен и четлив начин за изразяване на този поток на управление „назад и напред“.
От ниско ниво на C имплементации до преносими библиотеки
Един клас имплементации получава втори стек за повиквания ръчно и след това използва setjmp/longjmp за превключване между корутини. Специфичното за платформата вградено асемблиране може да създаде нов стек за всяка корутина; в POSIX системите, сигналите, комбинирани с sigaltstack може да се използва за първоначално изпълнение на алтернативен стек в чист C. След като всяка корутина има свой собствен стек, setjmp запазва състоянието на процесора и показалеца на стека, и longjmp възстановява ги, за да възобнови корутината.
Някои POSIX и UNIX-съвместими C библиотеки исторически са предлагали помощни функции, като например getcontext, setcontext, makecontext намлява swapcontext, които директно капсулират идеята за превключване между контексти на потребителско ниво. Въпреки че оттогава те са маркирани като остарели в POSIX.1-2008, те формират гръбнака на няколко библиотеки със сопрограми и вдъхновяват по-късни дизайни.
Заобикаляне на минималните реализации на корутини setjmp/longjmp и контекстни API изцяло, избирайки вместо това ръчно написан асемблер, който само разменя програмния брояч и показалеца на стека, унищожавайки други регистри. Това може да бъде драстично по-бързо при някои ABI, защото запазва точно това, което е необходимо, и нищо повече, докато setjmp трябва консервативно да съхранява по-голям набор от регистри.
За да се скрие цялата тази сложност от кода на приложението, през годините се появиха множество C библиотеки, които пакетират корутините, превключващи в чисти API-та, като например този на Ръс Кокс libtask и редица други (libpcl, coro, lthread, libcoro, libaco, libco и други). Тези библиотеки обикновено предоставят абстракции като леки задачи или влакна, които могат да бъдат възобновени и предоставени, без извикващата страна да се тревожи за основните трикове при асемблирането.
Приблизителни корутини в C с помощта на макроси
Когато отделни стекове или API за превключване на контекст не са налични или не са желателни, разработчиците също са апроксимирали корутините в чист C с макроси и switch оператори, техника, известна с документирането си от Саймън Тейтъм и свързана с класическия трик „устройството на Дъф“.
Основната идея е да се кодира състоянието на корутината като програмен брояч, реализиран с switch намлява case етикети, където всеки yield-like макрос се разширява до код, който записва текущия етикет в статична променлива и след това връща към извикващата функция. При следващото извикване функцията се връща към този етикет, вместо да започва отначало.
Библиотеки като Protothreads надграждат върху този модел, за да осигурят изключително леки, безстекови корутини, които се вписват в ограничени вградени среди, но подходът е съпроводен със сериозни ограничения: локалните променливи не се запазват естествено в yields, освен ако не се съхраняват в статични или външни структури, не можете лесно да прекъснете извикването на вложени функции и обикновено имате само една входна точка.
Дори поддръжниците му описват тази макро-базирана измама като един от най-грозните C кодове, използвани някога в продукцията, и критиците посочват, че полученият контролен поток може да бъде труден за обмисляне и поддържане във времето. Въпреки това, той остава полезен компромис в системи, където допълнителни стекове или трикове с линкери са изключени.
Стъпка: влакна, нишки и свързани абстракции
В масови среди, които нямат вградени корутини, нишките (и в по-малка степен влакната) са се превърнали в основен градивен елемент за паралелизъм. дори когато кооперативното поведение би било достатъчно. Нишките често са добре поддържани и добре документирани, но те решават по-широк и по-сложен проблем, отколкото повечето случаи на употреба на сопрограми всъщност изискват.
Влакната, където са налични, представляват по-близко съответствие с корутините на потребителско ниво, тъй като са кооперативно планирани и могат да се превключват без участието на операционната система. което ги прави естествен субстрат, върху който да се имплементират API-та в стил корутина. Системната поддръжка за влакна обаче е неравномерна в сравнение с нишките и преносимостта страда.
Една забележителна разлика между нишките и корутините е поведението при планиране. Нишките обикновено се прекратяват в произволни точки, което принуждава програмистите да разсъждават за условията на състезание и синхронизацията навсякъде. Корутините, за разлика от тях, променят контрола само в изрични точки на прекъсване, което често ви позволява да пишете по-прост код без заключвания или атомарни операции.
Езиците и средата за изпълнение са изследвали много пътища за емулиране на сопрограми върху съществуващата инфраструктура, от пренаписване на байткод (както в някои рамки за корутини на Java) до картографиране на подобни на корутини конструкции към итератори (както C# направи с yield преди async/await) или изграждането им върху зелени нишки, продължения или влакна.
Корутини в различните езици за програмиране
През десетилетията много езици са експериментирали с конструкции, подобни на корутини, всяка със собствен вкус и компромиси, и разбирането на тази екосистема помага да се постави еволюцията на C++ в контекст.
Някои езици предлагат първокласни, подредени корутини директно в библиотеката по време на изпълнение и стандартната библиотека. Lua, например, поддържа асиметрични, подредени по стек корутини от версия 5.0 чрез своя стандарт coroutine API, с примитиви за създаване, възобновяване и получаване. Modula-2 исторически е включвала поддръжка на корутини чрез процедури като NEWPROCESS намлява TRANSFER които настройват отделни стекове и превключват между контексти.
Други екосистеми изграждат корутини върху съществуващи примитиви като продължения или зелени нишки. Racket (и Scheme диалектите като цяло) могат да имплементират сопрограми почти тривиално, защото те предоставят продължения като първокласни стойности. Smalltalk системите, където стековете за изпълнение са манипулируеми обекти, могат по подобен начин да хостват абстракции на сопрограми без допълнителна поддръжка на виртуални машини. В OCaml кооперативната паралелност е осигурена чрез модули, които планират нишки превантивно в една нишка на операционната система, докато по-новите версии добавят поддръжка в стил „зелени нишки“.
Езиците, фокусирани върху асинхронно програмиране, често започваха с генератори, преди да въведат пълни корутини. C# първоначално добавя генератори чрез yield и моделът на итератора, след което еволюира в async/await да се моделират асинхронни операции като корутини. JavaScript следва подобен път: ES2015 въвежда генератори като специален случай на корутини, а по-късни версии добавят async/await изграден върху обещания и генератори.
В света на JVM, самата Java не предлага вградени корутини, но инструменти и езици около нея запълват празнината. Някои библиотеки модифицират байткода, за да симулират поведението на сопрограмите, други използват JNI за достъп до специфични за платформата механизми, а трети разчитат на нишки, за да емулират семантиката на сопрограмите на по-висока цена. Kotlin, от друга страна, предоставя сопрограмите като функция на библиотеката от първа страна и може да взаимодейства с Java код (въпреки че Java не може естествено да „спира“ и вместо това трябва да блокира или да използва фючърси).
Скриптовите и динамичните езици са възприели различни подходи. Python започна с подобрени генератори (PEP 342), разшири ги с делегиране на подгенератори (PEP 380) и в крайна сметка въведе явни нативни корутини с... async/await (PEP 492), като по-късно тези ключови думи са запазени в Python 3.7. Ruby имплементира подобно на корутини поведение чрез влакна (fibers); Raku и Tcl предлагат нативни конструкции на корутини; PHP 8.1 добави влакна (fibers), за да поддържа библиотеки, базирани на корутини, за асинхронен вход/изход.
Системно-ориентираните езици също изследват модели, подобни на корутини, със свой собствен обрат. Go използва горутини — леки, мултиплексирани процеси с динамично оразмерени стекове. Въпреки че горутините не са корутини в тесния смисъл на думата (те са по-близки до зелените нишки, а локалните данни не оцеляват след множество „извиквания“ в смисъла на корутина), те заемат подобно ментално пространство като задачите на потребителско ниво, управлявани от планировчик по време на изпълнение. D предоставя корутините чрез Fiber в стандартната си библиотека, а някои рамки ги обгръщат в удобни интерфейси, подобни на генератори.
Въведете C++: библиотеки преди стандарта
Преди C++ да стандартизира сопрограмите, екосистемата разчиташе на библиотеки на трети страни, за да внесе семантиката на сопрограмите в езика, използвайки комбинация от превключване на контекста на асемблер, платформени API и интелигентно метапрограмиране на шаблони.
Boost.Context се появи като ниско ниво основа за превключване на контексти на изпълнение между множество архитектури и операционни системи, предоставяйки преносим начин за манипулиране на стекове в потребителското пространство. В допълнение към това, Boost.Coroutine и по-късно Boost.Coroutine2 предлагаха абстракции на корутини от по-високо ниво, преминавайки от поддръжка както за симетрични, така и за асиметрични форми към по-модерен асиметричен интерфейс, който е по-добре съобразен със съвременните C++ идиоми.
Други проекти изследваха различни ъгли, като например базирани на препроцесор безстекови корутини, които емулират await/yield семантика, библиотеки с единичен заглавен файл, които обгръщат платформени влакна, или рамки (като корутините на Mordor или Oat++), които се фокусират специално върху скриване на асинхронни обратни извиквания на входно/изходни данни зад подобен на корутини последователен код.
Тези екосистеми демонстрираха, че разработчиците на C++ са жадни за изразителност, подобна на корутини, но те също така разкриха болезнените точки на ad-hoc решенията: непоследователен синтаксис, трудни проблеми с преносимостта, неудобно дебъгване и инструменти, както и нетривиална интеграция с останалата част от стандартната библиотека.
C++20 корутини: стандартизиран, безстеков модел
C++20 най-накрая въведе корутините в езика като първокласна функция, но с умишлено минималистичен и ниско ниво дизайн. Вместо да вмъква специфична абстракция от високо ниво (като „задача“, „генератор“ или „бъдеще“) в стандартната библиотека, C++20 стандартизира градивните елементи, които позволяват на библиотеките да дефинират свои собствени типове, удобни за корутини.
Функция става корутина, ако тялото ѝ съдържа някоя от специфичните за корутината конструкции: - co_await операторът да спре, докато някое събитие или стойност не е готова, co_yield израз за генериране на стойност и спиране (както в генераторите) или co_return оператор за завършване на корутината, по избор с резултат.
След като компилаторът открие някоя от тези конструкции, той трансформира функцията в машина на състоянията, чието постоянно състояние се съхранява в разпределена в heap „корутинна рамка“. освен ако оптимизацията не докаже, че времето на живот на кадъра е строго вложено в извикващата функция и може да бъде вградено в стековия кадър на извикващата функция. Този кадър съдържа promise обекта, копия на аргументите, локални променливи, които се намират в точките на спиране, и метаданни за водене на отчетност за възобновяване на изпълнението.
Най-важното е, че корутините в C++20 са без подреждане: корутина може да спира само в изрично изрично извършени точки на спиране (co_await or co_yield) и не могат прозрачно да се генерират от произволни вложени извиквания, освен ако тези функции са също сопрограми или по друг начин участват в механизма на сопрограмите. Това прави имплементацията по-проста и по-предсказуема, за сметка на известна изразителна сила в сравнение с напълно подредените дизайни.
Ограничения и жизнен цикъл на C++20 корутина
Не всяка функция в C++20 може да бъде корутина; стандартът налага няколко ограничения, за да се запази разумността на модела. Корутините не могат да бъдат constexpr or consteval функции, те не могат да бъдат конструктори, деструктори или main функция и те не могат да използват променливи аргументи в стил C или типове връщания на заместители като plain auto без допълнително уточнение.
Когато една корутина се извика за първи път, тя не се държи веднага като нормално тяло на функция. Вместо това, генерираният от компилатора пролог разпределя кадъра на корутината (обикновено чрез operator new), копира параметрите на функцията в този кадър (по стойност или по препратка, както е декларирано), конструира promise обекта и след това извиква promise.get_return_object(), което обикновено води до някакъв манипулатор или обвиващ обект, който се връща на извикващата функция.
Обектът promise е потребителски дефиниран тип, открит чрез std::coroutine_traits въз основа на типа на връщане и списъка с параметри на корутината, и той диктува как работят резултатите, изключенията и политиките за спиране. Компилаторът извежда Promise тип и след това извиква методи като initial_suspend(), final_suspend(), return_value() or return_void(), и unhandled_exception() в съответните фази от жизнения цикъл на сопрограмата.
В началото на изпълнението, корутината извиква promise.initial_suspend() намлява co_awaitкаквото и да се върне, което позволява на авторите на библиотеки да решат дали типът на тяхната сопрограма е „нетърпелива“ (започва да се изпълнява незабавно) или „мързелива“ (връща се към извикващия, докато не бъде изрично възобновена). Когато сопрограмата евентуално завърши чрез co_return или необработено изключение, то извиква promise.final_suspend(), което дава на библиотеката последен шанс да планира продължаване на изданията или да почисти.
Когато рамката на корутината бъде унищожена — или след завършване, или чрез изрична операция за унищожаване на нейния манипулатор — Изпълнителната среда унищожава promise обекта, копията на параметрите и всички останали активни локални параметри, след което освобождава паметта с operator delete (или със специфичен за обещанието разпределител, ако е предоставен). Ако разпределението е неуспешно и обещанието дефинира get_return_object_on_allocation_failure(), корутината може грациозно да сигнализира за неуспех, без да хвърля std::bad_alloc.
co_await, чакащи и чакащи
- co_await операторът е основният примитив за спиране в системата от корутини на C++20, и разбирането на неговите механизми е от решаващо значение за проектирането на стабилни асинхронни абстракции.
Когато пишете co_await expr; Вътре в корутина, компилаторът първо преобразува expr в „очакваем“ обект, или чрез прекарване през него promise.await_transform(expr) ако такъв член съществува или като го използва такъв, какъвто е. След това определя обекта „awaiter“ или чрез извикване на член operator co_await на очакваното, нечлен operator co_awaitили просто третиране на самия awaitable като awaiter, ако не съществува такъв оператор.
Сервитьорът трябва да осигури три ключови операции: await_ready(), await_suspend(handle) намлява await_resume(). If await_ready() връща true, корутината не спира и директно извиква await_resume(), което позволява бързи пътища за вече завършени операции. Ако върне false, корутината се спира, състоянието ѝ се съхранява в кадъра и await_suspend() се извиква с дескриптор на текущата корутина.
Вътре await_suspend(), чакащият може да реши какво да прави с манипулатора на корутината: планирайте го за по-късно възобновяване на някой изпълнител, възобновете друга сопрограма или дори възобновете същата сопрограма незабавно (в зависимост от типа на връщането и стойността на await_suspend()). Когато очакваната операция приключи, някой евентуално се обажда handle.resume(), при което контролът се връща към нивото точно преди await_resume()И след това await_resume() дава резултата от co_await изразяване.
Стандартната библиотека предоставя два тривиални awaitable-а: std::suspend_always намлява std::suspend_never, които често се използват в initial_suspend() намлява final_suspend() реализации, които да показват мързелив или нетърпелив старт и как да се държат в края. По-сложните чакащи могат да задържат състоянието на всяка операция, например, за да свържат сопрограмите с асинхронни I/O API, и това състояние се намира във рамката на сопрограмата през точката на спиране.
co_yield и корутини в стил генератор
- co_yield изразът се надгражда върху co_await да поддържа поведение, подобно на генератор, където корутината многократно генерира стойности за извикващия, който ги итерира.
Концептуално co_yield value; разширява се в призив към promise.yield_value(value) и след това суспензия, обикновено чрез co_await std::suspend_always или подобен awaitable. Имплементацията на promise е отговорна за съхраняването на получената стойност на някакво достъпно място (чрез копиране, преместване или референциране), така че потребителят да може да я извлече, преди корутината да възобнови изпълнението си.
Библиотечният код, който имплементира генератори, обикновено дефинира тип обещание, който предоставя методи за достъп до текущо получената стойност и за интегриране със стандартни протоколи за итерация. като например предоставяне begin()/end() върху обвивката на манипулатора и преместване на подлежащата корутина при всяко нарастване.
Обработка на грешки, висящи препратки и фини детайли
C++20 корутините се интегрират с обработката на изключения в C++ чрез promise-ите. unhandled_exception() метод, който компилаторът извиква, ако изключение излезе извън тялото на сопрограмата. След това сопрограмата преминава към окончателното си спиране и се очаква promise да уреди грешката да бъде съобщена на този, който притежава типа резултат на сопрограмата.
Тъй като параметрите се копират или се препращат в рамката на корутината по време на създаването ѝ, Трябва да се внимава с параметрите на референциите: ако те се отнасят до обекти, чийто живот приключва преди възобновяването на сопрограмата, сопрограмата може да разреференцира висящи референции. Това не е специфичен проблем за сопрограмата, но постоянният характер на рамката улеснява случайното надживяване на референцираните обекти.
Стандартът е еволюирал и за изясняване на крайни случаи чрез доклади за дефекти, като например да се направят някои невалидни return_void настройва неправилно, вместо да произвежда неопределено поведение при излизане от края на корутина, и позволява co_await в повече контексти като ламбда тела.
Заедно тези правила и подобрения оформят сравнително ниско ниво, но предвидим модел, върху който библиотеките с корутини от по-високо ниво могат безопасно да изградят, от прости генератори до пълни асинхронни/изчакващи системи за задачи, интегрирани с изпълнители и планировчици.
Погледнато отгоре, пътят от ad-hoc асемблиращи трикове в C до структурираните, безстекови, promise-базирани корутини на C++20 отразява постоянния стремеж към по-безопасни, по-композиционни абстракции за изразяване на сложен контролен поток, асинхронни операции и изчисления със състояние, без да се прави компромис с производителността и контрола, на които разчитат системните програмисти.