Knihovna libcoro - korutiny pro C++20 (revize)

24. 3. 2024 18:15 Ondřej Novák

Po určité době jsem se rozhodl revidovat svou knihovnu pro práci s korutinami v C++20. Během používání jsem si zapsal postřehy z dosavadního vývoje a tak vznikla nová knihovna která se snaží řešit některé problémy předchozí verze.

Motivace

Protože ani C++20 ani C++23 a vypadá to že ani C++26 nepřinese standardní nástroje na práci s korutinami a asynchronními operacemi, mým cílem bylo přijít s vlastním řešením v podobě této knihovny. Přitom jsem se snažil doručit minimum viable product s důrazem na maximální efektivitu a minimální množství tzv jalového kódu (to je kód, který pouze zajišťuje soudržnost navržených struktur)

Co se designu knihovny týče, snažím se používat programovací konvence STL knihovny, jako by knihovna měla ambice stát se součástí standardu (hodně teoreticky samozřejmě). Velký důraz je kladen na používání šablon a tím rozšíření možností při používání v různých algoritmech. Knihovna také nemá podobu frameworku, jako spíš propojení několika „užitečných tříd“.

Knihovna je navržena jako header-only. Po instalaci ať už jako system-wide modul nebo jako submodul by měl stačit jeden hlavičkový soubor

#include <coro.h>

Všechny deklarace najdete v namespace coro. Například coro::future  atd

Design knihovny je navržen s ohledem na vyšší míru abstrakce. Na low-level úrovni se při práci s korutinami setkáváte s coroutine_handle, awaiterysuspend_always, suspend_never, funkcemi jako jsou await_ready, await_suspendawait_resume, u generátorů s await_transfer a yield_value, a třeba s problematikou symetrického transferu. To všechno nepomáhá k jednoduchému uchopení tématu a proto se setkávám s tím, že mnoho programátorů se korutinám v C++ vyhýbá, protože jim prostě nerozumí. Mou motivací tak bylo celé téma přenést do abstraktnější úrovně a technické detaily implementace přenechat knihovně. I když si myslím, že i tyto detaily jsou důležité, rozhodně není je potřeba znát z počátku.

Různá designová rozhodnutí při implementaci byla provedena s ohledem na minimalizaci využívání heapu, resp. aby to kdy se použije heap, měl v plně v rukou uživatel. Pokud třeba používáte korutiny v prostředí, kde nelze alokovat paměť – myslím teď embedded prostředí, nebo prostředí s vysokou mírou bezpečnosti, kde třeba platí, že „po startu motoru se nesmí alokovat paměť“, pak libcoro se toto snaží maximálně naplnit.  Já vím, že třeba samotné korutiny předpokládají nějakou alokaci, ale i zde je možnost využít například alokátorů a rámce korutin nechat vytvářet v předalokovaných blocích.

Poznámka: V textu budu často srovnávat s knihovnu se starší revizí, která se na githubu jmenuje cocls. Pro novou revizi byl zvolen jiný název i namespace, protože jde prakticky o úplné přepsání „na zelené louce“

Co v knihovně nenajdete

Určitě zde nehledejte třídu „task“, která se jinak vyskytuje ve všech průvodcích korutinami, ačkoliv ve standardní knihovně se nikdy žádný std::task neobjevil. Pokud zabrousíte po internetu ve snaze najít konkurenční implementaci korutin, tak zcela jistě narazíte na lewissbaker/cppcoro (3.200 stars), kde najdete i implementaci „task“, ale třeba i „shared_task“ a když se ponoříte do zdrojového kódu a do návodu, jak se task používá, zjistíte takovou podivnou schizofrenii. Například si budete klást otázky, kdo je zodpovědný za život korutiny, kde je uložen výsledek korutiny, co se děje s korutinou, která skončila, atd… Podrobnou analýzou běhu korutiny například zjistíte, že korutina vlastně nemusí nikomu patřit, protože buď běží, pak je její lifetime jasně definován, nebo čeká, pak patří tomu, kdo je zodpovědný ji probudit, nebo skončila, přičemž se sama umí zničit. Jakmile odpovědnost za lifetime předáte externímu objektu, často se vám stane (jako mně), že korutinu zničíte v době, kdy na něco čeká (a ten někdo pak pracuje s dangling referencí)

Další problém je, že speciálně u cppcoro je výsledek uložen v rámci korutiny. A tak musíte rámec korutiny držet do té doby, dokud si chcete pamatovat výsledek, i když tento rámec již není potřeba. 

Task jako typ vám také moc nepomůže při návrhu API

virtual task<data> read_stream() = 0;

Tento řádek je učiněná katastrofa, pokud task je tedy chápán tak, jak je nadefinován v cppcoro, znamená to, že tato abstraktní funkce mne zavazuje k implementaci použít task. A to i přes to, že implementuji memory stream, který nemusí být implementován jako korutina.

Základní stavební kámen abstrakce „future“

Správné řešení (podle mne) nemám ze své hlavy, ale inspiroval jsem se v javascriptu. Doslova mi v C++ chybělo to, co javascript od určité verze má nativně. 

javascript libcoro
Korutiny se označují klíčovým slovem async v knihovně libcoro se používá async<> nebo future<>
Na výsledek async funkce se čeká await na výsledek future<> se čeká co_await
Místo korutiny mohu vracet new Promise instance future<> mohu zkonstruovat ručně
Na výsledek korutiny mohu použít .then() instance future<> má funkci .then() 
async function do_something () {
   await…;
   return…;
}
coro::future<T>  do_something() {
   co_await …;
   co_return …;
function do_something2() {
    return new Promise ((ok,err)=>{
       calc_async([ok,err]);
   }
}
coro::future<T> do_something2() {
    return [ ](auto promise) {
       calc_async(std::move(promise));
   }
}

A tím bychom mohli skončit. Přesněji, končí tím podobnost. V Javascriptu je to v zásadě všechno, co potřebujete k životu, v C++ ale spoustu věcí chybí. Například Javascript má vlastní dispatcher a systém řízení korutiny je plně v rukou javascriptového engine. V C++ žádný dispatcher není, tam má řízení korutin v rukou programátor.

Předně je třeba upozornit na vlastnosti objektu coro::future. Tento objekt není kopírovatelný ani přesouvatelný. Ve většině případů to nevadí, pokud například vstupuje jako proměnná do co_await, a ještě k tomu typicky jako dočasná proměnná (co_await read_stream()), tak nic takového nemusíme řešit. 

V prostředí mimo korutiny, kde nemáme co_await nabízí future možnost synchronního čekání

korutina: data d = co_await read_stream();
funkce:   data d = read_stream(); //will block

Co si pod tím představit? Jakmile je korutina zavolána, její vykonávání probíhá v aktuálním vlákně, dokud nenastane potřeba korutinu uspat. Korutina spí a čeká na probuzení, ale co s aktuálním vláknem? Z pohledu volajícího se volání korutiny tváří jako volání funkce, která se ukončila (vrátila) s prvním uspáním korutiny. V té době výsledek ještě není k dispozici. Protože se předpokládá, že uspání korutiny je v důsledku čekání na asynchronní operaci, která bude pokračovat v jiném vlákně, je synchronní čekání zařízeno zastavením celého vlákna a vyčkání na výsledek, jako by tam byla condition_variable.  Tady je třeba si dát pozor, aby korutina nečekala na akci, která se má stát v tomto vlákně, pak vyrobíte deadlock.

Třída coro::future tak nabízí ještě jednu možnost, jak mimo korutinu počkat na výsledek a to je pomocí funkce then a nastavení callbacku

auto fut = read_stream();
fut.then([&]{
    data d = fut;
});

Je potřeba si dát pozor na další omezení a to, že instanci future není povoleno destruovat před nastavením hodnoty. Nedodržení tohoto pravidla vede na std::terminate. Typicky je to problém právě u then, kdy sice nastavíme callback, ale pak kód opustí blok platnosti proměnné a program je přerušen. Naivním způsob řešení tohoto problému je alokovat proměnnou na heapu

auto fut = new auto(read_stream());
fut->then([fut]{
    data d= *fut;
    delete fut;
});

Další možností je použití ve třídách, které potřebují čekat na nějaké future a umí si „nějak“ zajistit svou vlastní existenci. Instance future mohou být deklarované jako členské proměnné třídy

class Foo {
public:
   coro::future<data> f;
   void process() {
       f << [&]{return read_stream();};
       f.then([this]{
           data d = f;
       })
   }
};

Protože coro::future není přesouvatelná, nebude fungovat přiřazení (přiřazení je buď kopií nebo přesunem, zde by se jednalo o přesun z dočasné instance představující výsledek volání asynchronní funkce, do existující instance). Rozhraní třídy coro::future nabízí přetížený operátor << který zde má význam jako „přesměrování výsledku“ a na své pravé straně přijímá funkci (invocable), která vrací instanci coro::future přesně stejného typu. Kód operátoru si funkci zavolá sám, zároveň však zajistí, aby se návratová instance konstruovala uvnitř existující proměnné.

Třída coro::future představuje základní stavební kámen pro synchronizaci asynchronní operace mezi volajícím a volaným bez ohledu na to, jestli na obou stranách jsou korutiny. Uživatel má plně ve svých rukou výběr technologie, kterou „nasadí“ na té které straně podle svých požadavků na efektivitu kódu. A nemusí jít jen o jednoho uživatele, dva programátoři se mohou snadno domluvit na asynchronním předávání výsledků aniž by jedna strana diktovala způsob implementace straně druhé.

Poznámka: Ještě je tu jedna odlišnost od Javascriptu, jedná se o zavedení limitu jednoho čekajícího. Zatímco u Javascriptu můžete objekt Promise kopírovat a můžete libovolně přes  then  registrovat další a další callbacky, u libcoro lze na jednu future může být pouze jeden čekatel současně.

Shrnutí

  • coro::future se nedá kopírovat ani přesouvat
  • instance nesmí být zničena před nastavením hodnoty
  • pouze 1 čekatel
  • z funkce se vrací konstrukcí v return
  • vrácenou future nelze přiřadit, místo toho existuje operátor přesměrování <<
  • na výsledek coro::future lze čekat pomocí co_await
  • přímé přiřazení coro::future do proměnné výsledku je synchronní (blokující) operace

Promise

Zatímco future je objekt se kterým pracuje volající, promise je objekt, se kterým pracuje volaný. Tento objekt představuje „dálkové ovládání“ vrácené future a umožňuje nastavit výsledek ve správný okamžik, jakmile je výsledek k dispozici. Objekt promise lze použít pouze jednorázově, jakmile je hodnota nastavena, objekt promise se stane neaktivní. Objekt lze přesouvat, přiřadit (přesunem) nebo dokonce zničit předčasně. Předčasné zničení je signalizováno na future jako speciální třetí stav, kdy svázaná future ukončí režim „pending“, ale zůstane bez hodnoty. Přístup pak vede na výjimku (await_canceled_exception), existuje ale způsob, jak tento stav detekovat i jinak.

Objekt promise se chová jako callback, takže jej lze snadno wrapnout například do std::function (invocable). Funkce přitom přijímá parametry ve stejném prototypu jako konstruktor typu, který future/promise konstruují. 

Zachycení výjimky

Asynchronní operace ale nemusí dopadnout správně, může dojít k výjimce a bylo by dobré tuto skutečnost sdělit přes future čekající korutině/funkci. Promise tak nabízí funkci .reject(výjimka) nebo .reject() v catch handleru. Výjimka je zachycena a když čekající požádá o výslednou hodnotu, obdrží výjimku ve formě  rethrow_exception

Plánování probouzení korutiny

V předchozích mých pokusech jsem se dost natrápil s plánovaním probouzení korutin, viz můj článek „jak se budí korutiny“. Toto jsem se snažil v libcoro vyřešit lépe, i když to znamená, že  částečně zodpovědnosti je přenesena do rukou uživatele programátora.

Nejprve ale v čem je problém? Nastavením promise zpravidla dojde k aktivaci korutiny a exekuci kódu, který následuje po co_await. Otázkou je, v rámci jakého kontextu se tento kód bude exekuovat?

Ve výchozím stavu to bude právě v kontextu toho vlákna, které provedlo nastavení hodnoty a to v ten okamžik, kdy došlo k nastavení. To není vždy žádoucí, protože okamžité probuzení korutiny může nějakým způsobem ovlivnit aktuální kontext, nebo aktuální kontext nemusí být vhodný pro běh kódu korutiny. Například může být aktivní zámek, který může blokovat současného exekutora, takže nemůže přijímat nové požadavky. A pokud probuzená korutina chce zařadit další asynchroní požadavek, dojde k deadlocku (je třeba zamknout exekutora k registraci čekající promise, ten je ale již zamknuty). Bylo by vhodné korutinu probudit až po odemknutí zámku. Ale zase by bylo vhodné nastavit hodnotu uvnitř zámku.

Knihovna libcoro toto řeší oddělením operace nastavení hodnoty od operace zaslání signálu a probuzení korutiny (notify).

Nastavením hodnoty (nebo výjimky) způsobí, že je vrácen objekt promise::notify který můžeme uložit a přenést mimo prostor zámku a tam notifikaci doručit. Pro doručení notifikace stačí objekt zničit, doručení se provede v destruktoru. Nebo můžeme objekt poslat do thread_poolu a tím korutině přidělíme vlákno a korutina tak běží v samostatném vlákně. 

Následující příklady ukazují různé způsoby odeslání signálu notify

void on_result_done(coro::promise<int> &prom) {
   std::lock_guard lk(_mx);
   prom(_result);     //dojde k probuzení pod zámkem
}

---

auto on_result_done(coro::promise<int> &prom) {
    std::lock_guard lk(_mx);
    return prom(_result); //probudí až volající - mimo zámek
}

---

void on_result_done(coro::promise<int> &prom) {
   //vytvoř thread a v něm probuď 
std::lock_guard lk(_mx); coro::promise<int>::notify ntf = prom(_result); std::thread thr(std::move(ntf)); thr.detach(); //detach threadu }

Objekt coro::promise::notify se chová jako funkce, jejímž zavoláním dojde k doručení signálu notifikace – a tedy k probuzeni korutiny nebo spuštění callbacku v aktuálním vlákně. Pokud je objekt zničen bez zavolání, signál notifikace se doručí z destruktoru. Tento objekt vrácený z funkce lze tedy bezpečně ignorovat s tím, že dojde k doručení notifikace na konci výrazu.

Shrnutí

  • Instance třídy promise drží referenci na pending future
  • Třída podporuje přesun, přiřazení a předčasnou destrukci
  • Třída se chová jako callable (invocable)
  • Přes instanci lze nastavit nejen hodnotu, ale i výjimku (metoda reject)
  • Nastavení hodnoty a nebo výjimky získáme objekt notify který umožňuje pozdržet notifikaci o nastavení hodnoty (a tím probuzení čekající korutiny) do okamžiku, kdy je to bezpečné.

Odložena future (deferred_future)

Odložená future je novinkou oproti předchozí revizi. Výhodou odložené future je ta, že dokud není hodnota požadována, není výpočet ani zahájen. 

Třída coro::deferred_future byla zavedena ze dvou důvodů. Prvním důvodem je zlepšení čitelnosti kódu. Pokud totiž v deklaraci rozhraní vidím deferred_future okamžitě vidím, že výpočet ještě nezačal a tedy neexistuje žádná reference na tuto proměnnou, není zde žádná paralelní úloha, která může do této future přistupovat (zatím).

Z tohoto důvodu jsou uvolněná některá omezení, například deferred_future je přesouvatelná a podporuje operátor přiřazení (=). Tento objekt lze také předčasně zničit, pak se odložený výpočet neprovede (zruší se).

Odložený výpočet je spuštěn jakmile je zahájeno čekání na výsledek, ať už přes co_await nebo přes synchronní čekání nebo přes then. Jakmile je odložený výpočet spuštěn, už se nesmí future přesouvat nebo zničit před dokončením výpočtu

Výhodou deferred_future je také to, že pokud obě strany rozhraní jsou korutiny, pak deferred_future zajišťuje předání řízení přes symmetric transfer a to oběma směry. Ani jedna strana přitom nemusí tuto informaci znát.

Z hlediska vztahu k coro::promise se nic nemění. Tento objekt používá stejný typ promise jako coro::future

Sdílená future (shared_future)

Sdílená future (coro::shared_future) představuje future, kterou lze sdílet kopírováním. Funguje to stejně jako shared_ptr. Každý držitel kopie pak smí svou kopii využít stejně jako by to byla jeho soukromá instance coro::future, tedy může čekat nebo nastavit callback. Toto je taky jediný objekt, který umožňuje aby na jednu proměnnou čekalo víc „čekatelů“, podmínkou je, že každý musí mít svou kopii sdílené future (na normální future smí čekat pouze jeden čekatel).

Sdílená future také umožňuje předčasné zničení, tedy v době, kdy výsledek ještě není k dispozici. Je to realizováno tak, že během čekání je držena jedna (soukromá) reference navíc, takže destrukcí všech veřejných referencí se nespustí destrukce future. To se stane až po nastavení hodnoty, kdy tato poslední reference je opuštěna

Z hlediska vztahu k coro::promise se ani tady nic nemění, Tento objekt používá stejný typ jako coro::future

Slučování promisí

Určitou novinkou je slučování promisí do jedné centrální promise, která pak ovládá mnoho čekajících future. Nejedná se přitom o shared_future, ale o obyčejné coro::future jejichž promisy jsou sloučené do jedné. Podmínkou k využítí je, aby typ T, který se tímto způsobem předává byl copy_constructible. Pak stačí nastavit jednu promise a hodnota se rozdistribuje všem čekajícím future.

Ke sloučení promisí se použije operátor + prostě jako by se chtěly promise sčítat. Definován je i operátor +=, tedy přidání další promise do kolekce

coro::promise<int> kolekce;
kolekce += a;
kolekce += b;
kolekce += c;
kolekce(42); //nastaví hodnotu všem třem promise a,b,c

Využití této vlastnosti se hodí například pro implementaci cache. Dokud není objekt v cache dostupný, například ve stavu, kdy se počítá, klíč v cache ale musí existovat. Tento klíč obsahuje promise prvního requestu, který výpočet požadoval. Pokud mezitím přijde další request na stejný objekt, je jeho promise sloučena s tou první promise a tím je operace vyřízena. Jakmile se výpočet dokončí, výsledek se uloží do cache a zároveň se nastaví hodnota uložené promise, čímž jsou všechny čekající požadavky vyřešeny. Každý další požadavek je pak okamžitě vyřešen z cache

Píšeme korutiny

Jak tedy psát korutiny? Uživatel knihovny nad tím nemusí přemýšlet. Prostě napíše funkci, která vrací jednu z výše uvedených future a uvnitř funkce použije co_await nebo co_return. (k vrácení hodnoty z korutiny přes future se právě hodí co_return. Pro coro::future<T> vracíme v co_return typ T. (pro coro::future<std::string>, vracíme v co_return std::string, případně cokoliv co lze do std::string převést)

coro::future<void> prazdna_korutina() {
    co_return;
}

Knihovna libcoro nabízí možnost deklarovat korutinu u které není dopředu rozhodnuto, přes jaký typ future se bude vracet výsledek (a zda vůbec). Taková korutina vrací typ coro::async<T>

coro::async<int> moje_coro() {
    co_return 42;
}

int main() {
   coro::future<int> f = moje_coro();
   int result = f;
   std::cout << result << std::endl;
}

Zavoláním korutiny vznikne objekt async<T>. Tělo korutiny se nespustí, ale korutina je připravená k běhu. Pro spuštění korutiny stačí vrácený objekt překonvertovat na future, deferred_future, nebo shared_future. V případě konverze na deferred_future také nedojde ke spuštění, ale bude možné korutinu spustit jakmile začne někdo na tomto typu future čekat. Po konverzi se objekt async<T> stane prázdný, a je možné jej zdestruovat.

coro::async<int, coro::pool_alloc> moje_coro() {...};

I tato korutina se spouští převedením na future, deferred_future nebo shared_future

Třída async dále podporuje možnost spustit korutinu v detached režimu. Tím se rozumí režim, kdy na dokončení korutiny není třeba čekat, kdy tedy korutina běží nezávisle na zbytku kódu. V tomto režimu není návratová hodnota dostupná, takže se ignoruje. Ignorují se i jakékoliv případné výjimky, které v korutině vzniknou.

moje_coro().detach();

Nejjednodušší korutina

Pro psaní korutin, které jsou vždycky spouštěné v detached režimu je deklarována korutina coro::coroutine. Toto je vůbec nejjednodušší korutina a i z hlediska množství jalového kódu jej obsahuje naprosté minimum

coro::coroutine jednoduchá_korutina() {
    co_await...;
    co_return;
}

Korutinu spustíme jako obyčejnou funkci. Korutina nic nevrací. Pozor na to, že korutina je považována jako by byla deklarována noexcept

jednoduchá_korutina(); //a běží hned

Generátory

Knihovna libcoro nabízí i standardní generátory. Generátory jsou korutiny, které používají klíčové slovo co_yield. Tato operace zastaví korutinu a předá výsledek volajícímu. Generátor lze pak zavolat znovu a běh korutiny pokračuje dál k dalšímu co_yield a výsledek je opět vrácen volajícímu. Klíčové slovo co_yield tak funguje jako co_return s tím rozdílem, že výpočet může pokračovat dál při dalším zavolání. 

coro::generator<int> fibo(int count) {
    unsigned int a = 1;
    unsigned int b = 1;

    for (int i = 0; i < count; ++i) {
        co_yield a;
        int c = a+b;
        a = b;
        b = c;
    }

}

int main() {
   for (int v: fibo(10)) {
      std::cout << v << std::endl;
   }
}

Knihovna libcoro umožňuje používat i asynchronní generátory, tedy generátory, které používají i co_await ve svém těle. Generátor lze volat jako funkci, která vrací coro::deferred_future a je tedy možné na výsledek generátoru čekat přes co_await

coro::future<void> main_async() {
    auto gen = fibo(10);
    auto f = gen();
    while (co_await !!f) {  //čekání a test, ze je vrácena hodnota
       int v = f;           //teď už synchronně
       std::cout << v << std::end;
       f = gen();           //deferred_future lze přiřadit
    }
}

poznámka: Operátor ! vrací true, pokud future nemá hodnotu. Příznak lze negovat, takže !! vrací true, pokud future má hodnotu. Aby bylo možné příznak testovat, musí být future ve stavu resolved. Protože je toto často používaná operace, je `co_await !!f ` syntaxtická zkratka, která umožňuje počkat na vyresolvení future a zároveň získat tento příznak jako jedna operace.

V tomto případě se k přepínání mezi korutinami používá symetrický transfer.

Agregátor generátorů

Pokud mám víc generátorů, které vrací stejný typ, pak je mohu agregovat. 

auto gen = coro::aggregator(gen1(), gen2(), gen3(),...)

Agregátor má využít pokud je použit s asynchronními generátory, tedy kdy agregátor vždy vrací hodnotu toho generátoru, který dřív spočítá svou hodnotu přičemž hodnoty dalších generátorů se řadí v pořadí jejich dokončení. Vždy po vyzvednutí hodnoty se generátor, jehož výsledek byl předán, znovu spustí aby vygeneroval další výsledek

Alokátor pro generátory

I generátor může použít alokátor: generátor<T, Allocator>. Aggregátor také přijímá alokátor, v takovém případě je předán jako první argument

Future jako awaiter

Určitou novinkou je možnost použít future jako obecný awaiter k jakémukoliv uživatelskému objektu. Jedná se zde o možnost použít future jako návratovou hodnotu přetíženého operátoru co_await

future<int> operator co_await() {return [&](auto promise) {...};}

Objekt, který definuje tento operátor se pak sám chová jako plnohodnotný awaiter, takže uživatel knihovny se vyhne programování vlastního awaiteru, což není triviální operace. Jako příklad uvedu mojí třídu na Websocket klienta, který navazuje spojení se serverem a vrátí websokectové spojení, když je handshake úspěšný. (jen interface)

class WebsocketClient {
public:
     WebsocketClient(HttpClientRequest &req);
     future<ws::Stream> operator co_await();
};

Použití:

{
   HttpClientRequest req = co_await client.GET("https://...");
   req.add_header("Authorization","...");
   ws::Stream stream = co_await WebsocketClient(req); //handshake
   ....
}

Tato vlastnost je umožněna zejména tím, že objekt coro::future nepoužívá přetížení operatoru co_await(), ale chová se přímo jako awaiter. To umožňuje objekt vracet jako výsledek přetíženého operátoru co_await() v jiné třídě.

Další možnosti řízení korutin

Řízení korutiny se neomezuje jen na zavolání a počkání na výsledek. Samotná korutina může být řízena dalšími nástroji, které jsou součástí výbavy libcoro

  • běžící korutině lze zasílat pokyny pomocí coro::queue. Korutina může fungovat jako dispatcher, nebo jako stavový stroj, který definuje svůj stav pomocí místa, kde došlo k uspání. 
  • korutinu lze ovládat také přes coro::distributor. Ten má schopnost distribuovat data i vícero korutinám současně a opakovaně. Jeho nevýhodou je, že je synchronní a pokud je korutina asynchronní, může propásnout některé hodnoty v době, kdy na ně nečeká. To lze řešit dodatečnou frontou
  • pokud korutiny musí přistupovat ke sdílenému prostředku, který je zároveň asynchronní (a zvládne jeden request současně), pak mohou pro řízení přístupu použít coro::mutex který podporuje co_await a je také bezpečné ho držet po dobu uspání a čekání na co_await
  • korutina se může plánovat přes coro::scheduler, který nabízí funkce sleep_for a sleep_until, přičemž čekání neblokuje vlákno
  • korutina se může kooperativně přepínat s jinými korutinami na stejném vlákně pomocí  co_await coro::pause()
  • korutinu lze synchronizovat přes libovolnou sdílenou proměnnou pomocí nástroje co_await coro::condition()coro::notify_condition()

Úplný seznam funkcí a tříd lze najít v referenční příručce u projektu.

Závěr

Myslím si, že toto je ideální místo, kde rychlé představení knihovny ukončit. Jít do detailu by zabral mnohem více místa a článek by byl velice dlouhý. Pro podrobnější studii tedy doporučuji navštívit stránky projektu na githubu. Budu rád za jakoukoliv kooperaci ve vývoji. Knihovna je a zůstane open source v licenci MIT.

Sdílet