- Coroutiner generaliserer subrutiner ved at bevare lokal tilstand og genoptage udførelsen ved suspensionspunkter, hvilket muliggør naturlig ekspression af tilstandsmaskiner, generatorer og kooperativ samtidighed.
- C-implementeringer udviklede sig fra manuel stakmanipulation og POSIX-kontekst-API'er til makrobaserede tilnærmelser og bærbare coroutine-biblioteker bygget på kontekstskift på brugerniveau.
- C++20 standardiserer en stakløs coroutine-model med løfter,
co_await,co_yieldog coroutine frames, der lader biblioteker definere asynkrone og generatorabstraktioner på højt niveau. - Den standardiserede model, kombineret med afventere og brugerdefinerede løftetyper, forener brugen af coroutine på tværs af biblioteker, samtidig med at den opretholder forudsigelig ydeevne og kontrol.
Coroutines ligger i en fascinerende mellemvej mellem klassiske funktioner og fuldendte tråde, og deres historie fra lavniveau-C-tricks til standardiseret C++20-sprogunderstøttelse er en af de mest interessante udviklinger inden for moderne systemprogrammering. Hvis du nogensinde har prøvet at jonglere med callbacks, tilstandsmaskiner og trådsynkronisering bare for at håndtere ikke-blokerende I/O, har du allerede mødt den slags smerte, som coroutines var designet til at lindre.
I denne artikel vil vi gennemgå, hvordan coroutines udviklede sig fra håndlavede C-hacks og POSIX-kontekst-API'er til den højniveau, stackless C++20 coroutine-model. forklarer hvad en coroutine egentlig er, hvordan den adskiller sig fra generatorer, tråde og fibre, hvad "stackful" vs. "stackless" betyder, og hvordan C++20-maskineriet (promise-objekter, coroutine-håndtag, co_await, co_yield, co_return) opfører sig faktisk under motorhjelmen.
Hvad er en coroutine egentlig?
Der findes ingen enkelt universelt accepteret formel definition af coroutine, men litteraturen konvergerer om to nøgleegenskaber, der adskiller koroutiner fra almindelige subrutiner:
- Lokal tilstand overlever på trods af suspensioner: Data lokale for en coroutine bevares mellem aktiveringer, så hver coroutine-instans opfører sig som et objekt med hukommelse.
- Udførelsen kan sættes på pause og senere genoptages fra samme punkt: Når kontrollen forlader en korutine, kan den genindføres ved det nøjagtige ophængningspunkt i stedet for altid at starte fra toppen som en normal funktion.
I stedet for at have en enkelt, one-shot ind- og udgangsfunktion som subrutiner, understøtter coroutines flere ind- og udgangspunkter i løbet af deres levetid, hvilket gør dem effektive til at udtrykke producenter, forbrugere, tilstandsmaskiner, kooperative planlæggere og asynkrone arbejdsgange i en lineær, læsbar stil.
Kernedimensioner af coroutine design
Virkelige koroutine systemer varierer langs tre vigtige akser, der definerer, hvordan de opfører sig, og hvor udtryksfulde de er: kontrol-overførselsmodellen, om koroutiner er førsteklasses værdier, og om de er stackful eller stackless.
For det første adskiller kontroloverføringsmekanismen asymmetriske fra symmetriske koroutiner. I et asymmetrisk design kan den aktive koroutine kun give tilbage til sin direkte opkalder (ved hjælp af en operation, der konceptuelt ligner yield), og den, der ringer op, genoptager den senere (med en handling svarende til resumeI symmetriske designs kan en korutine eksplicit overføre kontrol til enhver anden korutine i stedet for altid at vende tilbage til den, der kaldte den.
For det andet behandler nogle sprog coroutine-instanser som førsteklasses objekter, som du kan gemme, videregive og manipulere frit, mens andre kun eksponerer koroutiner som syntaktiske konstruktioner med begrænsede måder at interagere med dem på. Førsteklasses understøttelse øger fleksibiliteten og sammensætningsevnen dramatisk.
For det tredje kan koroutiner være stackfulde eller stackløse. En stackful coroutine kan suspendere dybt inde i en indlejret kaldstak; når den genoptages, fortsætter hver frame i stakken, hvor den slap. Stackless coroutines suspenderes kun på niveauet af selve coroutine-funktionen: almindelige hjælpefunktioner kan ikke give efter, medmindre de selv er coroutines eller er specielt annoterede.
Udtrykket "fuld coroutine" er blevet foreslået for den mest udtryksfulde kombination: en stablende, førsteklasses coroutine, enten symmetrisk eller asymmetrisk, hvilket er kraftfuldt nok til at udtrykke engangsfortsættelser eller afgrænsede fortsættelser. Selvom symmetriske og asymmetriske stilarter har tilsvarende udtrykskraft, føles den asymmetriske model ofte mere 'rutineagtig' og velkendt for de fleste programmører.
Subrutiner, koroutiner, generatorer og tråde
Subrutiner kan ses som et særligt tilfælde af korutiner med stærkt begrænset kontrolflow og tilstandsadfærd. En normal funktion starter altid ved sin første instruktion, afsluttes én gang og smider sin lokale tilstand væk bagefter. En coroutine kan derimod overføre kontrol til andre coroutines, genoptages senere ved flydepunktet og bevare sin tilstand på tværs af disse overførsler. Flere coroutine-instanser af den samme funktion kan sameksistere, hver med sine egne bevarede lokale data.
Generatorer danner en bemærkelsesværdig delmængde af koroutiner, undertiden kaldet "semikoroutiner". Ligesom coroutines kan generatorer suspendere udførelsen flere gange og senere fortsætte, men de giver altid efter for deres direkte kaldende funktion og har ingen måde at omdirigere udførelsen til en vilkårlig tredje coroutine. Denne begrænsning er bevidst: generatorer er optimeret til implementering af iteratorer og dovne sekvenser, hvor hver yield betyder simpelthen "producere en værdi for den, der iterer mig".
Faktisk kan du efterligne generelle korutiner ved at lægge en dispatcher oven på et generatorsystem, for eksempel ved at have en trampolin på øverste niveau, der modtager tokens fra generatorer og bestemmer, hvilken generator der skal aktiveres næste gang. Denne teknik blev historisk set brugt i sprog som tidlige Python-versioner, som kun havde generatorer, men ingen indbyggede coroutine-primitiver.
Coroutines sammenlignes ofte med tråde, men de handler grundlæggende om kooperativ planlægning snarere end præemptiv parallelisme. Coroutines giver samtidighed i den forstand, at de sammenfletter opgaver uden at ændre den overordnede semantik, men de kører ikke samtidigt på flere kerner alene. En coroutine giver kun kontrol ved eksplicitte suspensionspunkter, så kode mellem disse punkter kører uden afbrydelse fra andre coroutines.
Denne samarbejdsmodel eliminerer mange synkroniseringshovedpiner, der er almindelige med tråde: Fordi kun én coroutine kører ad gangen på en given scheduler, behøver du ofte ikke mutexer eller atomare operationer for almindelig delt tilstand. På den anden side vil coroutiner i sig selv ikke udnytte flere CPU-kerner, medmindre du kombinerer dem med tråde eller en multi-threaded executor.
Klassisk koroutineeksempel: producent-forbruger
En lærebogsdemonstration af symmetriske koroutiner er producent-forbruger-mønsteret med en delt kø. En korutine genererer elementer og skubber dem ind i en kø, indtil den er fuld, og giver derefter efter for forbrugeren; forbrugeren skubber elementer ind, indtil køen er tom, og giver derefter efter for producenten. Udførelsen hopper frem og tilbage, efterhånden som hver side i fællesskab giver kontrol.
I en sådan implementering ser producenten og forbrugeren ud til at køre "parallelt" fra programmørens perspektiv, selvom de faktisk bare hopper frem og tilbage inden for en enkelt udførelsestråd. Der er ikke behov for tråde på OS-niveau eller kontekstskift: yield-operationen kan være et lavniveauhop, der omkobler den aktive stakramme.
Dette eksempel bruges ofte til at introducere multithreading, men det er vigtigt at bemærke, at korutiner alene er tilstrækkelige til at udtrykke logikken, og at det kan være unødvendigt eller endda skadeligt at erstatte dem med tråde i miljøer, der lægger vægt på realtidsgarantier eller minimalt overhead for runtime.
Hvorfor coroutines er vigtige: tilstandsmaskiner, aktører og asynkrone arbejdsgange
Fordi koroutiner bevarer både deres udførelsespunkt og deres lokale variabler på tværs af udbytter, De giver en meget naturlig måde at implementere komplekse tilstandsmaskiner uden omfattende switch-sætninger, flag eller eksplicitte programtællere. Det aktuelle suspensionspunkt repræsenterer bogstaveligt talt den aktuelle tilstand.
Coroutines er også velegnede til aktør-lignende samtidighedsmodeller, såsom dem, der bruges i mange spilmotorer. Hver aktør kan implementeres som en coroutine, der periodisk giver kontrollen tilbage til en central scheduler, som kører den ene aktør efter den anden på en enkelt tråd. Denne kooperative multitasking eliminerer behovet for det meste låsning, samtidig med at den stadig giver responsiv adfærd.
Generatorer bygget på coroutines er ideelle til at arbejde med streams og datastrukturgennemstrømninger, især når du ønsker doven, on-demand produktion af værdier. I stedet for at skubbe værdier ind i en forbruger, lader en generator forbrugeren trække værdier én efter én ved hjælp af en simpel løkke.
Coroutines skinner også igennem i kommunikationsmønstre som pipelines og kommunikative sekventielle processer (CSP), hvor hvert trin er en coroutine, der giver efter, når den venter på input eller output. En scheduler genoptager derefter coroutines, når deres kommunikationskanaler er klar, hvilket giver et elegant alternativ til callback-tunge event-loops.
Endelig bruger mange numeriske biblioteker en stil, der undertiden kaldes "omvendt kommunikation", hvor en løsningsværktøj afbryder sig selv, når den har brug for, at brugeren skal give en funktionsevaluering, og derefter genoptager processen, når brugeren svarer. Coroutines giver en direkte og læsbar måde at udtrykke den frem-og-tilbage-kontrolstrøm på.
Fra lavniveau C-implementeringer til bærbare biblioteker
En klasse af implementeringer henter en anden kaldstak manuelt og bruger derefter setjmp/longjmp at skifte mellem korutiner. Platformspecifik inline-assembling kan oprette en ny stak for hver coroutine; på POSIX-systemer kombineres signaler med sigaltstack kan bruges til at bootstrappe udførelse på en alternativ stak i ren C. Når hver coroutine har sin egen stak, setjmp gemmer CPU-tilstanden og stakpointeren, og longjmp gendanner dem for at genoptage korutinen.
Nogle POSIX- og UNIX-justerede C-biblioteker har historisk set eksponeret hjælpefunktioner som f.eks. getcontext, setcontext, makecontext og swapcontext, som direkte indkapsler ideen om at skifte mellem brugerniveaukontekster. Selvom disse siden er blevet markeret forældede i POSIX.1-2008, dannede de rygraden i adskillige coroutine-biblioteker og inspirerede senere designs.
Minimale koroutine implementeringer omgår setjmp/longjmp og kontekst-API'er helt, vælger i stedet håndskrevet assembly, der kun bytter programtælleren og stakpointeren, hvilket overstyrer andre registre. Dette kan være dramatisk hurtigere på nogle ABI'er, fordi det sparer præcis det, der er nødvendigt, og intet mere, hvorimod setjmp skal konservativt lagre et større sæt af registre.
For at skjule al denne kompleksitet fra applikationskoden, er der gennem årene dukket flere C-biblioteker op, der har skiftet fra pakker til rene API'er. som f.eks. Russ Cox' libtask og en række andre (libpcl, coro, lthread, libcoro, libaco, libco og mere). Disse biblioteker leverer typisk abstraktioner som letvægtsopgaver eller fibre, der kan genoptages og leveres uden at den, der kalder, behøver at bekymre sig om de underliggende assembly-tricks.
Tilnærmelsesvise koroutiner i C ved hjælp af makroer
Hvor separate stakke eller kontekstskiftende API'er ikke er tilgængelige eller ønskelige, har udviklere også tilnærmet coroutines i ren C med makroer og switch-sætninger, en teknik berømt dokumenteret af Simon Tatham og relateret til det klassiske "Duff's device"-trick.
Kerneideen er at kode coroutinens tilstand som en programtæller implementeret med en switch og case etiketter, hvor hver yield-lignende makro udvides til kode, der registrerer den aktuelle etiket i en statisk variabel og derefter vender tilbage til den, der kalder. Ved næste kald hopper funktionen tilbage til den etiket i stedet for at starte fra begyndelsen.
Biblioteker som Protothreads bygger på dette mønster for at levere ekstremt lette, stakløse coroutiner, der passer i begrænsede indlejrede miljøer, men tilgangen har alvorlige begrænsninger: lokale variabler bevares ikke naturligt på tværs af udbytter, medmindre de er gemt i statiske eller eksterne strukturer, man kan ikke nemt suspendere fra indbyggede funktionskald, og man har generelt kun et enkelt indgangspunkt.
Selv dens fortalere beskriver dette makrobaserede trick som noget af den grimmeste C-kode, der nogensinde er brugt i produktion, og kritikere påpeger, at den resulterende kontrolstrøm kan være svær at ræsonnere omkring og vedligeholde over tid. Ikke desto mindre forbliver det et nyttigt kompromis i systemer, hvor yderligere stakke eller linkertricks er udelukket.
Trinbræt: fibre, tråde og relaterede abstraktioner
I mainstream-miljøer, der mangler native coroutines, er tråde (og i mindre grad fibre) blevet standardbyggestenen for samtidighed, selv når samarbejdsadfærd ville være tilstrækkelig. Tråde er ofte velunderbyggede og veldokumenterede, men de løser et bredere og mere komplekst problem, end de fleste korrutinemæssige use cases rent faktisk har brug for.
Fibre, hvor de er tilgængelige, giver et bedre match til brugerniveau-coroutiner, fordi de er kooperativt planlagte og kan skiftes uden OS-indblanding. hvilket gør dem til et naturligt substrat at implementere API'er i coroutine-stil på. Systemunderstøttelsen til fibre er dog ujævn sammenlignet med tråde, og portabiliteten lider.
En bemærkelsesværdig forskel mellem tråde og coroutines er planlægningsadfærd. Tråde bliver typisk præempteret på vilkårlige punkter, hvilket tvinger programmører til at ræsonnere om race conditions og synkronisering overalt. Coroutines ændrer derimod kun kontrol ved eksplicitte suspensionspunkter, hvilket ofte giver dig mulighed for at skrive enklere kode uden låse eller atomare operationer.
Sprog og runtime-programmer har udforsket mange måder at emulere coroutines oven på eksisterende infrastruktur, fra omskrivning af bytecode (som i nogle Java coroutine-frameworks) til at kortlægge coroutine-lignende konstruktioner til iteratorer (som C# gjorde med yield før async/await) eller bygge dem oven på grønne tråde, fortsættelser eller fibre.
Coroutiner på tværs af programmeringssprog
I årtierne har mange sprog eksperimenteret med coroutine-lignende konstruktioner, hver med sin egen smag og afvejninger, og forståelse af dette økosystem hjælper med at sætte C++-udviklingen i kontekst.
Nogle sprog tilbyder førsteklasses, stackable coroutines direkte i runtime- og standardbiblioteket. Lua har for eksempel understøttet asymmetriske, stackful coroutines siden version 5.0 via sin standard. coroutine API, med primitiver til at oprette, genoptage og udbytte. Modula-2 inkluderede historisk set coroutine-understøttelse gennem procedurer som NEWPROCESS og TRANSFER der opsætter separate stakke og skifter mellem kontekster.
Andre økosystemer byggede koroutiner oven på eksisterende primitiver som fortsættelser eller grønne tråde. Racket (og Scheme-dialekter generelt) kan implementere coroutines næsten trivielt, fordi de eksponerer fortsættelser som førsteklasses værdier. Smalltalk-systemer, hvor eksekveringsstakke er manipulerbare objekter, kan ligeledes hoste coroutine-abstraktioner uden ekstra VM-understøttelse. I OCaml er kooperativ samtidighed blevet leveret via moduler, der planlægger tråde præemptivt på en enkelt OS-tråd, mens nyere versioner tilføjer understøttelse af green-thread-stil.
Sprog fokuseret på asynkron programmering startede ofte med generatorer, før de introducerede fulde korutiner. C# tilføjede oprindeligt generatorer gennem yield og iteratormønsteret, udviklede sig derefter til async/await at modellere asynkrone operationer som koroutiner. JavaScript fulgte en lignende vej: ES2015 introducerede generatorer som et særligt tilfælde af koroutiner, og senere versioner tilføjede async/await bygget oven på løfter og generatorer.
I JVM-verdenen tilbyder Java ikke selv native coroutines, men værktøjer og sprog omkring det udfylder hullet. Nogle biblioteker ændrer bytekode for at simulere coroutine-adfærd, andre bruger JNI til at få adgang til platformspecifikke mekanismer, og nogle er afhængige af tråde til at emulere coroutine-semantik til en højere pris. Kotlin tilbyder derimod coroutines som en førstepartsbiblioteksfunktion og kan interagere med Java-kode (omend Java ikke naturligt kan "suspendere" og i stedet skal blokere eller bruge futures).
Scripting og dynamiske sprog har taget forskellige tilgange. Python startede med forbedrede generatorer (PEP 342), udvidede dem med subgeneratordelegering (PEP 380) og introducerede til sidst eksplicitte native coroutines med async/await (PEP 492), og reserverede senere disse nøgleord i Python 3.7. Ruby implementerer coroutine-lignende adfærd via fibre; Raku og Tcl tilbyder native coroutine-konstruktioner; PHP 8.1 tilføjede fibre for at understøtte coroutine-baserede biblioteker til asynkron I/O.
Systemorienterede sprog udforsker også coroutine-lignende modeller med deres eget twist. Go bruger goroutines — lette, multipleksede processer med dynamisk store stakke. Selvom goroutines ikke er coroutines i streng forstand (de er tættere på grønne tråde, og lokale data overlever ikke flere 'kald' i coroutine-forstand), optager de et lignende mentalt rum som brugerniveauopgaver, der administreres af en runtime scheduler. D eksponerer coroutines via Fiber i sit standardbibliotek, og nogle frameworks pakker dem ind i praktiske generator-lignende grænseflader.
Indtast C++: biblioteker før standarden
Før C++ standardiserede coroutines, var økosystemet afhængig af tredjepartsbiblioteker for at bringe coroutine-semantik ind i sproget, ved hjælp af en blanding af assembler-kontekstskift, platform-API'er og smart skabelon-metaprogrammering.
Boost.Context fremstod som et lavniveau-fundament til at skifte udførelseskontekster på tværs af flere arkitekturer og operativsystemer, hvilket gav en bærbar måde at manipulere brugerrumsstakke på. Derudover tilbød Boost.Coroutine og senere Boost.Coroutine2 coroutine-abstraktioner på højere niveau, hvor de gik fra understøttelse af både symmetriske og asymmetriske former til en mere moderne asymmetrisk brugerflade, der bedre stemmer overens med moderne C++-idiomer.
Andre projekter udforskede forskellige vinkler, såsom præprocessorbaserede stakløse coroutiner, der emulerer await/yield semantik, single-header-biblioteker, der omslutter platformfibre, eller frameworks (som Mordor- eller Oat++-coroutines), der specifikt fokuserer på at skjule asynkrone I/O-tilbagekald bag coroutine-lignende sekventiel kode.
Disse økosystemer viste, at C++-udviklere var sultne efter coroutine-lignende udtryksevne, men de afslørede også smertepunkterne ved ad hoc-løsninger: inkonsekvent syntaks, vanskelige problemer med portabilitet, akavet fejlfinding og værktøjer og ikke-triviel integration med resten af standardbiblioteket.
C++20 coroutines: en standardiseret, stakløs model
C++20 bragte endelig coroutines ind i sproget som en førsteklasses funktion, men med et bevidst minimalistisk og lavniveau-design. I stedet for at indsætte en specifik abstraktion på højt niveau (såsom "opgave", "generator" eller "fremtid") i standardbiblioteket, standardiserede C++20 de byggesten, der giver biblioteker mulighed for at definere deres egne coroutine-venlige typer.
En funktion bliver en coroutine, hvis dens brødtekst indeholder en af de coroutine-specifikke konstruktioner: og co_await operatøren suspenderer, indtil en begivenhed eller værdi er klar, co_yield udtryk for at producere en værdi og suspendere (som i generatorer), eller co_return en sætning for at fuldføre coroutinen, eventuelt med et resultat.
Når compileren registrerer en af disse konstruktioner, transformerer den funktionen til en tilstandsmaskine, hvis persistente tilstand er gemt i en heap-allokeret "coroutine frame", medmindre optimering beviser, at framens levetid er strengt indlejret i kalderen og kan indlejres i kalderens stakframe. Denne frame indeholder promise-objektet, kopier af argumenter, lokale variabler, der lever på tværs af suspensionspunkter, og bogføringsmetadata til genoptagelse af udførelsen.
Afgørende er det, at C++20 coroutines er stackless: En coroutine kan kun suspenderes ved eksplicitte suspensionspunkter (co_await or co_yield) og kan ikke transparent give udbytte fra vilkårlige indbyggede kald, medmindre disse funktioner også er korutiner eller på anden måde deltager i korutinemaskineriet. Dette gør implementeringen enklere og mere forudsigelig, på bekostning af en vis udtrykskraft sammenlignet med fuldt stackful designs.
Begrænsninger og livscyklus for en C++20-koroutine
Ikke alle funktioner i C++20 må være en coroutine; standarden pålægger adskillige begrænsninger for at holde modellen sund. Coroutines kan ikke være constexpr or consteval funktioner, de kan ikke være konstruktører, destruktører eller main funktion, og de må ikke bruge C-stil variadiske argumenter eller pladsholderreturtyper som almindelig auto uden yderligere specifikation.
Når en coroutine kaldes første gang, opfører den sig ikke umiddelbart som en normal funktionskroppe. I stedet allokerer den compiler-genererede prolog den korutine ramme (normalt via operator new), kopierer funktionsparametre ind i den frame (efter værdi eller ved reference som deklareret), konstruerer promise-objektet og kalder derefter promise.get_return_object(), hvilket typisk giver et eller andet handle- eller wrapper-objekt, der returneres til den, der kalder.
Promise-objektet er en brugerdefineret type, der opdages gennem std::coroutine_traits baseret på coroutinens returtype og parameterliste, og den dikterer, hvordan resultater, undtagelser og suspenderingspolitikker fungerer. Compileren udleder en Promise skriv og kalder derefter metoder som initial_suspend(), final_suspend(), return_value() or return_void()og unhandled_exception() i de passende faser i coroutine-livscyklussen.
Ved starten af udførelsen kalder coroutinen promise.initial_suspend() og co_awaithvad end det returnerer, hvilket giver biblioteksforfattere mulighed for at bestemme, om deres coroutine-type er "eager" (starter med det samme) eller "lazy" (vender tilbage til den, der kalder den, indtil den eksplicit genoptages). Når coroutinen endelig afsluttes via co_return eller en uhåndteret undtagelse, påkalder den promise.final_suspend(), hvilket giver biblioteket en sidste chance for at planlægge fortsættelser eller rydde op.
Når coroutine-rammen ødelægges — enten efter færdiggørelse eller via en eksplicit ødelæggelsesoperation på dens handle — Kørselstiden ødelægger promise-objektet, kopierne af parametrene og eventuelle resterende live locals og frigør derefter hukommelsen med operator delete (eller med en løftespecifik allokator, hvis tilgængelig). Hvis allokeringen mislykkes, og løftet definerer get_return_object_on_allocation_failure(), kan korutinen elegant signalere fiasko uden at kaste std::bad_alloc.
co_await, ventende og opvartende
co_await operatoren er den primære suspensionsprimitiv i C++20's koroutinesystem, og forståelse af dens mekanik er afgørende for at designe robuste asynkrone abstraktioner.
Når du skriver co_await expr; Inde i en coroutine konverterer compileren først expr til et "venteværdigt" objekt, enten ved at føre den igennem promise.await_transform(expr) hvis et sådant medlem findes, eller ved at bruge det som det er. Derefter bestemmer den "waiter"-objektet enten ved at kalde et medlem operator co_await på det ventende, et ikke-medlem operator co_await, eller blot at behandle den afventende operator som den afventende operator, hvis en sådan operator ikke findes.
Tjeneren skal udføre tre nøgleoperationer: await_ready(), await_suspend(handle) og await_resume(). If await_ready() returnerer sandt, coroutinen suspenderer ikke og kalder direkte await_resume(), hvilket muliggør hurtige stier for allerede gennemførte operationer. Hvis den returnerer falsk, suspenderer coroutinen, dens tilstand gemmes i rammen, og await_suspend() kaldes med et handle til den aktuelle coroutine.
Indendørs await_suspend(), kan tjeneren bestemme, hvad de vil gøre med coroutine-handlen: planlæg den til senere genoptagelse på en eller anden udførende proces, genoptag en anden coroutine, eller genoptag endda den samme coroutine med det samme (afhængigt af returtypen og værdien af await_suspend()). Når den ventede operation er færdig, ringer nogen til sidst handle.resume(), hvor kontrollen vender tilbage til lige før await_resume(), Og derefter await_resume() giver resultatet af co_await ekspression.
Standardbiblioteket leverer to trivielle ventetider: std::suspend_always og std::suspend_never, som ofte bruges i initial_suspend() og final_suspend() Implementeringer til at indikere doven eller ivrig start og hvordan man skal opføre sig til sidst. Mere sofistikerede ventefunktioner kan holde en tilstand pr. operation, for eksempel for at binde coroutines til asynkrone I/O API'er, og den tilstand befinder sig inde i coroutine-rammen på tværs af suspensionspunktet.
co_yield og generatorlignende koroutiner
co_yield udtryk bygger oven på co_await at understøtte generatorlignende adfærd, hvor korutinen gentagne gange producerer værdier for en kalder, der itererer over dem.
Begrebsmæssigt co_yield value; udvider sig til et opkald til promise.yield_value(value) og så en suspension, typisk via co_await std::suspend_always eller en lignende afventelig værdi. Løfteimplementeringen er ansvarlig for at lagre den afgivne værdi et tilgængeligt sted (ved at kopiere, flytte eller referere til den), så forbrugeren kan hente den, før korrutinen genoptages.
Bibliotekskode, der implementerer generatorer, definerer almindeligvis en løftetype, der eksponerer metoder til at få adgang til den aktuelt leverede værdi og til integration med standard iterationsprotokoller. såsom at levere begin()/end() på håndtagsindpakningen og fremføre den underliggende korutine for hvert trin.
Fejlhåndtering, dinglende referencer og subtile detaljer
C++20-koroutiner integreres med C++-undtagelseshåndtering gennem løftets unhandled_exception() metode, som compileren kalder, hvis en undtagelse undslipper coroutine-kroppen. Coroutine fortsætter derefter til sin endelige suspension, og løftet forventes at sørge for, at fejlen kommunikeres til den, der ejer coroutine-resultattypen.
Fordi parametre kopieres eller refereres til coroutine-rammen på oprettelsestidspunktet, Der skal udvises forsigtighed med referenceparametre: hvis de refererer til objekter, hvis levetid slutter, før coroutinen genoptages, kan coroutinen muligvis udereferencere dinglende referencer. Dette er ikke et coroutine-specifikt problem, men framens vedvarende natur gør det lettere ved et uheld at overleve refererede objekter.
Standarden har også udviklet sig til at afklare kanttilfælde via fejlrapporter, såsom at gøre visse ugyldige return_void dårligt formede opsætninger i stedet for at producere udefineret adfærd, når de falder ud af enden af en korutine, og tillader co_await i flere sammenhænge som lambda-kroppe.
Sammen former disse regler og forbedringer en relativt lavniveau, men forudsigelig model, som coroutine-biblioteker på højere niveau sikkert kan bygge videre på. lige fra simple generatorer til komplette asynkrone/afventende opgavesystemer integreret med udførende og planlæggere.
Set ovenfra, rejsen fra ad-hoc assembly-tricks i C til de strukturerede, stakløse, løftedrevne coroutiner i C++20 afspejler et konstant skub mod sikrere og mere sammensættelige abstraktioner til at udtrykke komplekse kontrolflow, asynkrone operationer og tilstandsfulde beregninger, uden at give afkald på den ydeevne og kontrol, som systemprogrammører er afhængige af.