Het retourneren van std::coroutine_handle vanuit await_suspend maakt symmetrische overdracht mogelijk, een vorm van gegarandeerde tail-call optimalisatie (TCO). Wanneer await_suspend void retourneert, moet de coroutine-runtime terugkeren naar de aanroepende functie voordat de volgende coroutine wordt hervat, wat een geneste aanroepstack creëert die lineair groeit met de ketenlengte. Door een handle te retourneren, genereert de compiler een directe sprong (jmp instructie) naar het hervatpunt van de doel-coroutine, waardoor het huidige activatierekord wordt hergebruikt en een constante O(1) stackdiepte wordt behouden, ongeacht de ketenlengte.
struct SymmetricTransfer { std::coroutine_handle<> next; // Tail-call geoptimaliseerd: geen stackgroei std::coroutine_handle<> await_suspend(std::coroutine_handle<>) { return next; } void await_resume() {} bool await_ready() { return false; } };
We hebben een realtime audioprocessing-engine ontwikkeld voor professionele muziekproductiesoftware. Het systeem gebruikte C++20 coroutines om een pijplijn van 500+ digitale signaalverwerkings (DSP) effecten (filters, compressors, reverb) aan elkaar te koppelen. Tijdens stress-testen crasht de applicatie met een stack overflow bij het laden van complexe effect racks, ondanks dat elke individuele coroutine een minimale lokale toestand had.
Oplossing 1: Void-retourerende await_suspend met directe hervatting De initiële implementatie gebruikte void await_suspend(std::coroutine_handle<>) en riep next.resume() intern aan. Deze aanpak bood een intuïtieve, sequentiële codeflow en gemakkelijke debugging via standaard stack traces. Echter, elke resume() oproep genest binnen de opschortinglogica van de vorige coroutine, consumeerde ongeveer 16KB per fase en uitgeputte de 8MB thread stack na slechts 500 fasen.
Oplossing 2: Werkqueue met asynchrone planning We overwogen om directe chaining te vervangen door een gecentraliseerde taakqueue waar elke coroutine de volgende fase als een werkitem indiende en onmiddellijk opschorte. Dit garandeerde constant stackgebruik door recursie om te zetten in iteratie. Het nadeel was een significante prestatieverslechtering: dynamische allocaties voor queue-knooppunten, cache-thrashing door thread-concurrentie, en verlies van cache-localiteit tussen pijplijnfasen, wat onze sub-milliseconde latentievereisten schond.
Oplossing 3: Symmetrische overdracht via coroutine_handle We hebben await_suspend herschreven om de std::coroutine_handle van de volgende fase direct terug te geven. Dit signaleerde de compiler om TCO uit te voeren, waardoor de stackframes samenvielen. De oplossing behield de nul-kosten abstractie van coroutines, terwijl het O(1) geheugengebruik garandeerde. Het primaire risico hield verband met lifetime management: eenmaal de handle was geretourneerd, was de huidige coroutine opgeschort, en toegang tot this of lokale variabelen na het terugkeerpunt resulteerde in ongedefinieerd gedrag.
Gekozen Oplossing en Resultaat We hebben Oplossing 3 aangenomen. Na de herschrijving verwerkte de pijplijn met succes 512 opeenvolgende effecten met slechts 4KB stackruimte, waardoor crashes werden geëlimineerd en deterministische realtime-prestaties werden behouden. De wijziging vereiste zorgvuldige codebeoordelingen om ervoor te zorgen dat er geen post-terugkeerlogica bestond in await_suspend, maar resulteerde in een robuuste, schaalbare architectuur.
Waarom vereist symmetrische overdracht het retourneren van std::coroutine_handle in plaats van het gebruik van co_await op de volgende coroutine binnen await_suspend?
Het gebruik van co_await binnen await_suspend zou vereisen dat de wachtende coroutine volledig is opgeschort, en later weer wordt hervat, wat inherent inhoudt dat er naar de runtime moet worden teruggekeerd en de stack moet groeien. Het direct retourneren van de handle stelt de compiler in staat om de hervatting als een tail call te beschouwen, terwijl co_await een asymmetrisch opschortingpunt genereert dat het frame van de aanroepende functie moet behouden om het later opnieuw te hervatten.
Hoe beïnvloedt symmetrische overdracht de uitzonderingveiligheid als de hervatte coroutine een uitzondering gooit vóór het bereiken van zijn finale opschortingspunt?
Als de symmetrisch overgedragen coroutine een uitzondering gooit, ontrafelt de uitzondering zich conceptueel door het await_suspend-frame, maar aangezien de oorspronkelijke coroutine al als opgeschort is gemarkeerd, moet het frame worden vernietigd tijdens het ontrafelen van de stack. Dit vereist dat de compiler complexe uitzondering-handling-tabellen genereert die de belofte en gevangen parameters van de opgeschorte coroutine vernietigen. Kandidaten missen vaak dat op maat gemaakte promise_type allocators de gedeeltelijke constructie correct moeten afhandelen, anders riskeren zij dubbele vernietiging bugs tijdens het ontrafelen van de uitzondering.
Wat voorkomt het gebruik van symmetrische overdracht bij het implementeren van een generator die waarden uit een recursieve datastructuur oplevert?
Generators vertrouwen op co_yield om de controle terug te geven aan de aanroepende functie terwijl ze hun staat behouden. Symmetrische overdracht geeft onvoorwaardelijk de controle door aan een andere coroutine en keert nooit terug naar de oorspronkelijke aanroepende functie totdat de gehele keten is voltooid. Daarom moeten generators asymmetrische opschorting gebruiken (door void of true terug te geven vanuit await_suspend) om de consument in staat te stellen de geleverde waarde te ontvangen en mogelijk de generator later opnieuw te hervatten, in plaats van een onomkeerbare overdracht naar een andere coroutine af te dwingen.