Від C-хаків до C++20: Еволюція корутин

Останнє оновлення: 12/27/2025
Автор: C SourceTrail
  • Корутини узагальнюють підпрограми, зберігаючи локальний стан та відновлюючи виконання в точках призупинення, забезпечуючи природний вираз кінцевих автоматів, генераторів та кооперативного паралельного виконання.
  • Реалізації на C еволюціонували від ручного маніпулювання стеком та API контексту POSIX до макроорієнтованих апроксимацій та портативних бібліотек корутин, побудованих на перемиканні контексту на рівні користувача.
  • C++20 стандартизує модель безстекової корутини з обіцянками, co_await, co_yield та фрейми корутин, що дозволяє бібліотекам визначати високорівневі асинхронні та генераторні абстракції.
  • Стандартизована модель у поєднанні з очікувачами та користувацькими типами промісів уніфікує використання корутин у різних бібліотеках, зберігаючи при цьому передбачувану продуктивність та контроль.

еволюція корутин з C на C++

Корутини займають захопливу золоту середину між класичними функціями та повноцінними потоками, і їхня історія від низькорівневих трюків на C до стандартизованої підтримки мови C++20 є однією з найцікавіших еволюцій у сучасному системному програмуванні. Якщо ви коли-небудь намагалися поєднувати зворотні виклики, кінцеві автомати та синхронізацію потоків лише для обробки неблокуючого вводу/виводу, ви вже стикалися з тим болем, для полегшення якого були розроблені корутини.

У цій статті ми розглянемо, як корутини еволюціонували від ручних хаків C та API-інтерфейсів контексту POSIX до високорівневої моделі корутин C++20 без стеку. пояснення того, що насправді являє собою корутина, чим вона відрізняється від генераторів, потоків та волокон, що означає «стекована» проти «стекованої», та як працює механізм C++20 (об'єкти promise, обробники корутин, co_await, co_yield, co_return) насправді поводиться «під капотом».

Що ж таке корутина насправді?

características en profundidad csharp
Пов'язана стаття:
Поглиблені функції, екосистема та інструменти C#

Не існує єдиного загальноприйнятого формального визначення корутини, але література сходиться на двох ключових властивостях, які відрізняють корутини від звичайних підпрограм:

  • Місцевий стан виживає попри призупинення діяльності: Дані, локальні для корутини, зберігаються між активаціями, тому кожен екземпляр корутини поводиться як об'єкт з пам'яттю.
  • Виконання може бути призупинено, а потім відновлено з тієї ж точки: Коли керування залишає корутину, в неї можна знову ввійти точно в точці призупинення, замість того, щоб завжди починати зверху, як у звичайній функції.

Замість одноразового входу та виходу, як у підпрограм, корутини підтримують кілька точок входу та виходу протягом свого життєвого циклу, що робить їх потужними для вираження виробників, споживачів, кінцевих автоматів, кооперативних планувальників та асинхронних робочих процесів у лінійному, читабельному стилі.

Основні виміри дизайну корутин

Реальні системи корутин відрізняються за трьома важливими напрямками, які визначають їхню поведінку та виразність: модель передачі керування, чи є корутини значеннями першого класу, та чи є вони стековими чи безстековими.

По-перше, механізм передачі керування розділяє асиметричні та симетричні корутини. В асиметричному дизайні активна корутина може повертатися лише безпосередньому викликачеві (використовуючи операцію, концептуально подібну до yield), а абонент відновлює його пізніше (за допомогою операції, подібної до resume). У симетричних дизайнах корутина може явно передати керування будь-якій іншій корутині, а не завжди повертатися до того, хто її викликав.

По-друге, деякі мови програмування розглядають екземпляри корутин як об'єкти першого класу, які можна зберігати, передавати та вільно маніпулювати ними, тоді як інші представляють корутини лише як синтаксичні конструкції з обмеженими способами взаємодії з ними. Першокласна підтримка значно підвищує гнучкість та компонуємість.

По-третє, корутини можуть бути стековими або безстековими. Стекована корутина може призупинитися глибоко всередині вкладеного стеку викликів; коли вона відновлює роботу, кожен кадр у цьому стеку продовжується з того місця, де він зупинився. Безстекові корутини призупиняються лише на рівні самої функції корутини: звичайні допоміжні функції не можуть повернути результат, якщо вони самі не є корутинами або спеціально анотовані.

Термін «повна корутина» було запропоновано для найвиразнішої комбінації: стекова корутина першого класу, симетрична або асиметрична, що достатньо потужно для вираження одноразових або розділених продовжень. Хоча симетричні та асиметричні стилі мають однакову виразну силу, асиметрична модель часто здається більш «рутинною» та знайомою більшості програмістів.

Підпрограми, корутини, генератори та потоки

Підпрограми можна розглядати як окремий випадок корутин із суворо обмеженим потоком керування та поведінкою станів. Звичайна функція завжди починається зі своєї першої інструкції, завершується один раз і скидає свій локальний стан після цього. Корутина, навпаки, може передавати керування іншим корутинам, відновлюватися пізніше в точці повернення та зберігати свій стан протягом цих передач. Кілька екземплярів корутин однієї й тієї ж функції можуть співіснувати, кожен зі своїми власними збереженими локальними даними.

Генератори утворюють помітну підмножину корутин, які іноді називають «напівкорутинами». Як і корутини, генератори можуть призупиняти виконання кілька разів, а потім продовжувати, але вони завжди повертаються до свого прямого викликаючого елемента та не мають можливості перенаправити виконання на довільну третю корутину. Це обмеження навмисне: генератори оптимізовані для реалізації ітераторів та лінивих послідовностей, де кожен yield просто означає «створювати цінність для того, хто мене повторює».

Фактично, ви можете емулювати загальні корутини, розмістивши диспетчер поверх системи генераторів, наприклад, використовуючи батут верхнього рівня, який отримує токени від генераторів і вирішує, який генератор активувати наступним. Цей метод історично використовувався в мовах програмування, таких як ранні версії Python, які мали лише генератори, але не мали вбудованих примітивів корутин.

Корутини часто порівнюють з потоками, але вони по суті стосуються кооперативного планування, а не випереджувального паралелізму. Корутини забезпечують паралельність у сенсі чергування завдань без зміни загальної семантики, проте вони не виконуються одночасно на кількох ядрах самі по собі. Корутина надає керування лише в явних точках призупинення, тому код між цими точками виконується без переривання з боку інших корутин.

Ця кооперативна модель усуває багато проблем із синхронізацією, характерних для потоків: Оскільки в даному планувальнику одночасно виконується лише одна корутина, часто не потрібні м'ютекси чи атомарні операції для звичайного спільного стану. З іншого боку, корутини самі по собі не використовуватимуть кілька ядер процесора, якщо ви не поєднаєте їх з потоками або багатопотоковим виконавцем.

Класичний приклад корутини: виробник-споживач

Хрестоматійною демонстрацією симетричних корутин є шаблон «виробник-споживач» зі спільною чергою. Одна корутина генерує елементи та заштовхує їх у чергу, доки вона не спорожніє, а потім передає їх споживачеві; споживач виштовхує елементи, доки черга не спорожніє, а потім повертається до виробника. Виконання відбувається поступово, оскільки кожна сторона спільно передає керування.

У такій реалізації, з точки зору програміста, виробник і споживач працюють «паралельно». хоча насправді вони просто перемикаються вперед і назад в межах одного потоку виконання. Немає потреби в потоках рівня ОС або перемиканнях контексту: операція yield може бути низькорівневим переходом, який перепідключає активний стековий кадр.

Цей приклад часто використовується для введення багатопоточності, але важливо зазначити, що одних лише корутин достатньо для вираження логіки, і що їх заміна потоками може бути непотрібною або навіть шкідливою в середовищах, які піклуються про гарантії реального часу або мінімальні накладні витрати на виконання.

Чому корутини важливі: кінцеві автомати, актори та асинхронні робочі процеси

Оскільки корутини зберігають як точку виконання, так і локальні змінні в усіх 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-подібний макрос розширюється до коду, який записує поточну мітку у статичній змінній, а потім повертає її до викликаючої сторони. Під час наступного виклику функція повертається до цієї мітки замість того, щоб починати з початку.

Бібліотеки, такі як Protothreads, базуються на цьому шаблоні, щоб забезпечити надзвичайно легкі, безстекові корутини, які підходять для обмежених вбудованих середовищ, але цей підхід має серйозні обмеження: локальні змінні природним чином не зберігаються в yield, якщо вони не зберігаються в статичних або зовнішніх структурах, ви не можете легко призупинити виконання вкладених викликів функцій, і зазвичай у вас є лише одна точка входу.

Навіть його прихильники описують цей макро-алгоритм як один із найпотворніших кодів на C, які коли-небудь використовувалися у продакшені. а критики зазначають, що отриманий потік керування може бути важко обґрунтувати та підтримувати з часом. Тим не менш, він залишається корисним компромісом у системах, де додаткові стеки або трюки з лінкерами неможливі.

Сходинка: волокна, нитки та пов'язані абстракції

У масових середовищах, де відсутні власні корутини, потоки (і, меншою мірою, волокна) стали стандартним будівельним блоком для паралельності, навіть коли достатньо кооперативної поведінки. Потоки часто добре підтримуються та документуються, але вони вирішують ширшу та складнішу проблему, ніж це насправді потрібно більшості випадків використання корутин.

Волокна, де вони доступні, ближче відповідають корутинам рівня користувача, оскільки вони плануються спільно та можуть перемикатися без участі ОС. що робить їх природним субстратом для реалізації API у стилі корутин. Однак системна підтримка волокон є нерівномірною порівняно з потоками, і страждає портативність.

Одна помітна відмінність між потоками та корутинами полягає в поведінці планування. Потоки зазвичай витісняються в довільних точках, що змушує програмістів розмірковувати про умови гонки та синхронізацію всюди. Корутини, навпаки, змінюють керування лише в явних точках призупинення, що часто дозволяє писати простіший код без блокувань або атомарних операцій.

Мови програмування та середовища виконання досліджували багато шляхів емуляції корутин поверх існуючої інфраструктури, від переписування байт-коду (як у деяких фреймворках корутин Java) до відображення конструкцій, подібних до корутин, на ітератори (як це робив C# з yield перед тим async/await) або нарощуючи їх поверх зелених ниток, продовжень чи волокон.

Корутини в різних мовах програмування

Протягом десятиліть багато мов експериментували з конструкціями, подібними до корутин, кожна з яких мала свої особливості та недоліки. і розуміння цієї екосистеми допомагає розглянути еволюцію C++ у контексті.

Деякі мови пропонують першокласні, стекові корутини безпосередньо в бібліотеці виконання та стандартній бібліотеці. Наприклад, Lua підтримує асиметричні, стекові корутини з версії 5.0 через свій стандарт coroutine API з примітивами для створення, відновлення та отримання. Modula-2 історично включала підтримку корутин через такі процедури, як NEWPROCESS та TRANSFER що встановлюють окремі стеки та перемикаються між контекстами.

Інші екосистеми створювали корутини поверх існуючих примітивів, таких як продовження або зелені потоки. Racket (і діалекти Scheme загалом) можуть реалізовувати корутини майже тривіально, оскільки вони надають продовження як значення першого класу. Системи Smalltalk, де стеки виконання є маніпульованими об'єктами, можуть аналогічно розміщувати абстракції корутин без додаткової підтримки віртуальних машин. В OCaml кооперативна паралелізм забезпечується за допомогою модулів, які планують потоки превентивно в одному потоці ОС, тоді як новіші версії додають підтримку стилю green-thread.

Мови, орієнтовані на асинхронне програмування, часто починалися з генераторів до появи повноцінних корутин. Спочатку 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 додав волокон для підтримки бібліотек на основі корутин для асинхронного вводу/виводу.

Системно-орієнтовані мови також досліджують моделі, подібні до корутин, зі своїм власним підходом. 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 оператор для завершення корутини, необов'язково з результатом.

Як тільки компілятор виявляє будь-яку з цих конструкцій, він перетворює функцію на кінцевий автомат, постійний стан якого зберігається в виділеному купою «кадрі корутини». якщо оптимізація не доведе, що час життя фрейму суворо вкладений у викликаючу функцію та може бути вбудований у стековий фрейм викликаючої функції. Цей фрейм містить об'єкт обіцянки, копії аргументів, локальні змінні, що зберігаються в точках призупинення, та метадані обліку для відновлення виконання.

Найважливіше те, що корутини 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) якщо такий член існує, або використовуючи його як є. Потім він визначає об'єкт "waiter" або викликаючи член operator co_await на очікуваному, нечлен operator co_await, або просто обробка самого об'єкта очікування як об'єкта очікування, якщо такого оператора не існує.

Очікуючий повинен забезпечити три ключові операції: 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 вираз.

Стандартна бібліотека постачає два тривіальні об'єкти очікування: std::suspend_always та std::suspend_never, які часто використовуються в initial_suspend() та final_suspend() реалізації для позначення лінивого або нетерплячого початку та поведінки в кінці. Більш складні очікувачі можуть зберігати стан для кожної операції, наприклад, для прив'язки корутин до асинхронних API вводу/виводу, і цей стан знаходиться всередині кадру корутини через точку призупинення.

co_yield та корутини у стилі генератора

Команда co_yield вираз будується поверх co_await для підтримки поведінки, подібної до генератора, де корутина неодноразово генерує значення для викликаючої функції, яка виконує ітерацію по них.

Концептуально, co_yield value; розширюється у виклик до promise.yield_value(value) а потім підвіска, зазвичай через co_await std::suspend_always або подібний об'єкт очікування. Реалізація проміса відповідає за зберігання отриманого значення в доступному місці (шляхом копіювання, переміщення або посилання на нього), щоб споживач міг отримати його до відновлення виконання корутини.

Бібліотечний код, що реалізує генератори, зазвичай визначає тип обіцянки, який надає методи для доступу до поточного отриманого значення та для інтеграції зі стандартними протоколами ітерації. такі як надання begin()/end() на обгортці дескриптора та просування базової корутини з кожним приростом.

Обробка помилок, висячі посилання та тонкі деталі

Корутини C++20 інтегруються з обробкою винятків C++ через проміси. unhandled_exception() Метод, який компілятор викликає, якщо виняток виходить за межі тіла корутини. Потім корутина переходить до свого остаточного призупинення, і очікується, що проміс організує повідомлення про помилку тому, хто володіє типом результату корутини.

Оскільки параметри копіюються або посилаються на них у кадрі корутини під час створення, Слід бути обережним з параметрами посилань: якщо вони посилаються на об'єкти, термін життя яких закінчується до відновлення роботи корутини, корутина може розіменувати висячі посилання. Це не є проблемою, специфічною для корутини, але постійна природа фрейму полегшує випадкове переживання об'єктів, на які посилаються.

Стандарт також розвинувся для уточнення крайніх випадків за допомогою звітів про дефекти, такі як визнання певних недійсними return_void налаштовує неправильно сформовані параметри замість невизначеної поведінки при виході з кінця корутини та дозволяє co_await у більшій кількості контекстів, таких як лямбда-тіла.

Разом ці правила та вдосконалення формують відносно низькорівневу, але передбачувану модель, на якій можуть безпечно будувати бібліотеки корутин вищого рівня, від простих генераторів до повноцінних систем асинхронних/очікувальних завдань, інтегрованих з виконавцями та планувальниками.

Зверху видно шлях від спеціальних асемблерних трюків у C до структурованих, безстекових, керованих promise-орієнтованих корутин C++20 відображає постійний рух до безпечніших, більш компонуємих абстракцій для вираження складного потоку керування, асинхронних операцій та обчислень з урахуванням стану, без шкоди для продуктивності та контролю, на які покладаються системні програмісти.

Схожі повідомлення: