Princip async/await z jazyka C# 5.0

Tomáš Holan       24.09.2012       C#, Threading       47899 zobrazení


Nedávno jsem došel k názoru, že je dost lidí (čti vývojářů), kteří ještě úplně přesně nechápou princip a způsob vykonávání metod zapisovaných pomoci nové syntaxe async/await z jazyka C# 5.0. Tento článek je proto určen hlavně pro ně. Doufám, že se mi v něm podaří tuto problematiku alespoň trochu objasnit.


V tomto článku si na velmi jednoduchém příkladu vysvětlíme princip vykonávání kódu používající novou syntaxi jazyka C# 5.0 async/await.

Tuto syntaxi lze asi nejčastěji využít v aplikacích s grafickým rozhraním na některé ze XAML-based platformě:

  • WPF a .NET Framework 4.5
  • Silverlight 5.0 s Async Targeting Pack
  • Windows Store Apps a WinRT (aplikace pro Windows 8 “Modern UI”)
  • Windows Phone 8

Mějme metodu provádějící nějaký velmi náročný výpočet:

private int Compute()
{
    System.Threading.Thread.Sleep(3000);
    return 42;
}

A protože je tento výpočet tak náročný, možná by bylo dobré mít k dispozici také asynchronní verzi této metody:

private Task<int> ComputeAsync()
{
    return Task.Run(() =>
        {
            System.Threading.Thread.Sleep(3000);
            return 42;
        });
}

Jakým způsobem metoda dokáže to, že je asynchronní, nám v mnoha případech může zůstat uvnitř metody skryto (1). Důležité je však to, že ze signatury metody resp. přímo toho, že její návratová hodnota je typu Task nebo Task<T> poznáme, že je jedná o asynchronní metodu (2).

(Datový typ Task je nyní v .NETu primárním datovým typem reprezentující asynchronní operace s tím, že všechny dřívější reprezentace asynchronních operací jsou na tuto reprezentaci principiálně převeditelné.)

Zde se v tomto konkrétním případě jedná o task/úlohu, která po svém dokončení vrací hodnotu typu int a dosahuje se toho obalením synchronního kódu pomoci Task.Run(() => … ) (3), což způsobí jeho vykonávání na threadpool.

Všimněte si také, že jsme vytvořili asynchronní metodu, aniž by bylo nutné nějaké async nebo await použít a to tak, že vracíme (nějakým způsobem sestrojený) task pomoci obyčejného return. Jak doufám hned uvidíme dále, async/await je “pouze” pomůcka pro umožnění jiného způsobu zápisu vlastní implementace těla asynchronní metody (4). Opět to jestli je tělo asynchronní metody zapsané pomoci async/await nebo ne je její implementační detail.

Dále mějme kód vlastní aplikace (aplikace má velmi jednoduché grafické rozhraní s prvky Button, ProgressBarTextBlock):

private async void Button_Click(object sender, RoutedEventArgs e)
{
    this.IsEnabled = false;
    progress.IsIndeterminate = true;
    try
    {
        await RunComputeAndShowResults();
    }
    finally
    {
        this.IsEnabled = true;
        progress.IsIndeterminate = false;
    }
}

private async Task RunComputeAndShowResults()
{
    textBlock.Text = "Running...";

    //Async computation
    int result = await ComputeAsync();

    //Update UI
    textBlock.Text = result.ToString();
}

Funkčnost celé aplikace je velice jednoduchá. Tlačítko spouští výše uvedený asynchronní výpočet, po jehož dokončení je výsledek zobrazen v TextBlocku. Kromě toho jsou během provádění výpočtu prvky formuláře disablované, je zobrazena indikace průběhu a text “Running…”.

Ještě než přistoupíme k vysvětlení způsobu vykonávání kódu příkladu, ve stručnosti shrnu hlavní pravidla týkající se použitých nových klíčových slov:

  • async – Tímto klíčovým slovem označíme v deklaraci metodu, v jejímž těle budeme chtít alespoň jednou použít klíčové slovo await čímž aktuální metodu budeme implementovat jako kompozici/orchestraci běhu asynchronního workflow sestavenou z volání jiných existujících asynchronních funkcí.
    Tím změníme způsob jejího překladu do stavového automatu, který bude zajišťovat spouštění částí kódu za a mezi jednotlivými await jako callback (continuation) jednotlivých asynchronních volání.
    Metoda označená klíčovým slovem async musí vracet typ Task, Task<T> nebo případně void.
    V metodě označené klíčovým slovem async příkaz return určuje okamžik dokončení asynchronní úlohy a v případě návratového typu Task<T> se za něj uvádí pouze výraz typu T – výstup z asynchronní metody.
  • await – Toto klíčové slovo uvedeme před asynchronní volání, kdy chceme, aby se další běh kódu umístěného za await naplánoval (jako continuation) a spustil až po dokončení volané operace. V důsledku toho jsou v kódu za await již k dispozici výsledky prováděného asynchronního volání (tj. v případě metody vracející Task<T> je k dispozici hodnota typu T).
    Klíčové slovo await lze uvést pouze před výraz typu Task nebo Task<T> (5).

Pomocná metoda RunComputeAndShowResults je asynchronní, vrací typ Task, protože přestavuje úlohu, která nemá po svém dokončení žádnou návratovou hodnotu, ale přitom chceme umět na okamžik jejího dokončení reagovat. Metoda je implementována pomoci nového způsobu async/await, kde klíčové slovo await uvádíme před voláním vlastního výpočtu metodou ComputeAsync(), protože teprve po jeho dokončení chceme provést aktualizaci uživatelského rozhraní pro zobrazení výsledku výpočtu.

Metoda Button_Click (event handler prvku Button) sice také používá způsob implementace pomoci async/await, ale vrací pouze void, takže se nejedná o asynchronní metodu v tom smyslu, že by na její dokončení šlo nějak reagovat (také nejde). Toto je přesto naprosto validní postup v případě, kdy nám stačí pouze umět voláním metody spouštět asynchronního workflow, které je s výhodou implementované pomoci async/await v jejím těle (top level funkce). Zde opět pomoci await provádíme nějaké akce až po dokončení běhu asynchronní metody RunComputeAndShowResults (nikoliv po dokončení jejího volání – volání například
var t = RunComputeAndShowResults(); by skončilo ihned a vrátilo by v daném čase spuštěný, ale ještě nedokončený task).

Jaká je tedy v našem příkladu posloupnost a průběh jednotlivých akcí, které se dějí po stisku tlačítka na formuláři?

  1. Vyvolá se procedura event handleru Button_Click
  2. Synchronně se provedou akce před (prvním) await (prvky formuláře se disablují a zobrazí se indikace průběhu) a zavolá se metoda RunComputeAndShowResults.
  3. Synchronně se pokračuje prováděním akcí před await v těle metody RunComputeAndShowResults (zobrazí se nápis “Running…”) (6), potom se zavolá metoda ComputeAsync.
  4. Metoda ComputeAsync spustí výpočet na pomocném vlákně na threadpool a ihned vrátí objekt Task<int> reprezentující tento asynchronně spuštěný a běžící výpočet.
  5. Metoda RunComputeAndShowResults převezme objekt vrácený metodou ComputeAsync a nastaví spuštění kódu za await (to je realizováno pomoci stavového automatu) jako callback/continuation  tohoto tasku (7). Dále vrátí objekt Task reprezentující asynchronně spuštěnou a běžící úlohu [výpočet + zpracování jeho výsledků].
  6. Metoda Button_Click převezme objekt vrácený metodou RunComputeAndShowResults a opět nastaví spuštění další části metody za await jako callback/continuation vracenému tasku.
  7. Běh metody Button_Click končí (a protože tato metoda je typu void, není objekt reprezentující celou úlohu nikam vrácen).

Všechny akce až do tohoto okamžiku proběhli najednou a synchronně. Hlavní vlákno není v tomto okamžiku ničím blokováno a může zpracovávat libovolné jiné události. Na threadpool stále běží spuštěný asynchronní výpočet.

  1. V nějakém čase později dojde v pomocném vláknu na threadpool k dokončení výpočtu.
  2. Výsledek výpočtu je nastaven do objektu Task<int> reprezentujícího úlohu tohoto výpočtu a je vyvolán callback, který byl této úloze dříve nastaven, tj. zbytek metody RunComputeAndShowResults. Tento callback je ve výchozím chování vyvolán na původním synchronizačním kontextu (na kterém bylo prováděno volání asynchronní metody) tj. v našem případě na UI (main) vlákně.
  3. Provede se zbytek metody RunComputeAndShowResults (zobrazení výsledku výpočtu). Přitom je k dispozici výsledek již dokončeného výpočtu a protože jsme na UI vlákně není aktualizace UI problém.
  4. Objektu Task reprezentující úlohu [výpočet + zpracování jeho výsledků] je signalizováno jeho dokončení a je opět vyvolán jemu dříve nastavený callback tj. zbytek metody Button_Click.
  5. Provede se zbytek metody Button_Click (enablování prvků formuláře a skrytí indikace průběhu).

Tím jsou všechny akce dokončeny.

Pokud by tělo některé metody používalo klíčové slovo await vícekrát, princip bude stejný, jen přerušení zpracování a vrácení kontroly hlavnímu vláknu se provede v celém běhu spuštěných akcí několikrát (pro každé volání dílčí asynchronní metody).


(1) Je to implementační detail dané metody. Obecně se může jednat například o procesorově náročný výpočet běžící na thread pool/worker threadu, paralelní výpočet běžící na několika threadech (procesorech), asynchronní volání WCF nebo jiné služby nebo cokoliv jiného.

(2) I když ve speciálních případech (v závislosti na vstupních parametrech, stavu objektu, výjimečně vždy) může taková metoda proběhnout celá synchronně a vrátit již dokončený Task nebo Task<T>.

(3) Helper metoda Run je zkratka pro Task.Factory.StartNew(() => … ) z TPL.

(4) Je to naprosto analogické jako u iterátorů z C# 2.0. Tam také implementaci metody vracející nějakou sekvenci (IEnumerable<T>) můžeme zapsat buď jako iterátor blok (případ async/await), ale také například jen vrátit vytvořený LINQ dotaz (případ vrácení objektu typu Task).

(5) Případně jiný tzv. “awaitable” objekt tj. objekt implementující metodu GetAwaiter().

(6) Pokud by jsme na tomto místě volali například metodu Compute():

textBlock.Text = "Running...";

//Sync computation
int result = Compute(); //<--Blocks UI (Main) thread

//Async computation
result = await ComputeAsync();

bude výpočet prováděn synchronně na UI (main) vlákně aplikace tj. hlavní vlákno bude po celou dobu provádění výpočtu zablokováno.

(7) Pokud by v tento moment (v našem případě tomu tak ale není) vracený objekt task  signalizoval, že reprezentovaná úloha je již dokončena, nastavení callbacku by se neprovádělo, ale místo toho by se rovnou (synchronně s předcházejícími akcemi) pokračovalo zpracováváním kódu za await. Tato “optimalizace” se označuje jako tzv. “fast path”.

 

hodnocení článku

3 bodů / 3 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

ConfigureAwait

V první řadě chci říct, že je to pěkný článek a vše je dobře vysvětlené.

Jen malá připomínka. Nebylo by od věci zmínit ConfigureAwait. Hlavně v tomto příkladu, který využívá UI a může způsobit deadlock.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Stejně mi zatuhne

Díky za článek,

vzorová úloha mi funguje perfektně.

Ale jakmile nahradím:

System.Threading.Thread.Sleep(3000);

Za nějakou skutečnou operaci (v mém případě komunikace přes USB trvající asi 10s) tak na tuto dobu mi opravdu celý form zatuhne...

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Poděkování

Díky za článek, dobře vysvětleno!

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Jak to je?

Článek mi moc nepomohl, protože se tady píše, že ta asynchronní část běží v jiném vlákně. Takto jsem to taky chápal a pak je to jednoduché a jasné.

Ale tady Microsoft píše, že u async/await se žádná další vlákna nepoužívají a ta asynchronní metoda neběží ve svém vlastním vlákně ale v tom vlákně, které ji volalo. A tím pádem tam není žádný paralelismus a není mi jasné, co se urychlí.

https://msdn.microsoft.com/library/hh191...

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Jediné co v mém příkladu běží na jiném vlákně je část

{
    System.Threading.Thread.Sleep(3000);
    return 42;
}

uvnitř volání Task.Run (nikoliv celá metoda ComputeAsync).

Async/await opravdu žádná vlákna netvoří ani nic jiného s nimi nedělá. To co může na to, že konkrétně tato část běží v threadpool vlákně je pouze to volání Task.Run.

Async/await je pouze nástroj nebo pomůcka pro kompozici asynchronních metod tak, aby kód mohl mít stejnou strukturu, jako jsme zvyklí u synchronního kódu.

Ještě k tématu async/await doporučuji záznam z přednášky "Tasky a await pěkně od základu a jednoduše", záznam je k dispozici zatím z Brna (z Prahy bude vystaven snad během pár dnů), zde:

http://wug.cz/zaznamy/350-Tasky-a-await-...

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Paralelismus tam je v tom, že celá aplikace nezatuhne ale bude reagovat při čekání než se dokončí příkaz System.Threading.Thread.Sleep(3000); Pochopitelně k tomu aby aplikace mohla reagovat ji musíme dát funkce které uživatel může při čekání na dokončení výpočtu provádět.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Dost dobrý článek

Uctivě děkuji. Zatím není důvod jásat nad nabytými vědomostní ale mám pocit, že díky tomuhle vše pochopím.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Super

Super. Dik za prehladne vysvetlenie.

Nejaky piatok som s tym nerobil, ale prave som riesil problem kde som to chcel ("musel") pouzit.

Po precitani jasne na prvu supu.

Pevne nervy a nenechaj sa odradit "odbornikmi" ako tu v diskusii :-)

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Díky.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Článek o ničem

Článek snaživý a pěkně napsaný, ale úplně na hovno, protože nemluví o to, k čemu to do 3,14či vlastně je dobré. Takových článků o tom, jak se používá await async, jsou mraky. Ty napíšeš:

await RunComputeAndShowResults();

Ok, takže se ti ta metoda spustí v jiném vlákně a ty na ní čekáš. A co má jako kuwa být? Stejně sis tím await zastavil běh hlavního vlákna, tak k čemu tam ten paralelismus potom je? Pochopil bych ještě tohle:

Task t = RunComputeAndShowResults();
// 
// tady rychle delam nejake dalsi picoviny
//
t.wait(); //a tady uz musim pockat nez mi dobehne Task t

A pak se ptám naco je mi await a naco označuješ tu metodu jako async. Chápeš, že ten tvůj sáhodlouhý ukecaný článek, nad kterým jsem promarnil 10 minut života vč. toho, že tě tu teď musím zbuzerovat, je úplně o hovně?

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Dobrý den,

Předně i když jste třeba z nějaké technologie frustrovaný, není to důvod psát na tomto portálu sprostě a vulgárně. Je to v přímém rozporu s podmínkami, jak se na tomto portálu chovat, a pokud nic jiného, riskujete tím, že Váš příspěvek bude smazán.

Ve článku jsem se pokusil právě zaměřit na tu stránku jak async/awail funguje principiálně, nikoliv na to k čemu je to dobré, to právě naopak na rozdíl od principu je uvedeno úplně všude. Nicméně to, že async/awail je pomůcka, která slouží k usnadnění psaní kódu, pokud v kódu potřebujeme provádět asynchronní volání, jsem ve článku uvedl. Pokud žádné asynchronní volání ve svém kódu provádět nepotřebujeme, async/awail není pro Vás. Pokud potřebujeme paralelismus, async/awail Vám ho nezajistí.

Z Vašeho dotazu je vidět jednak, že patříte přesně do té kategorie programátorů, pro kterou byl článek určený, ale bohužel zadruhé že ve Vašem případě Vám ani moje snaha (zatím) nepomohla, protože jste článek zřejmě vůbec nečetl (jak vyplyne níže).

Konkrétně k Vašemu kódu.

await RunComputeAndShowResults();

"Ok, takže se ti ta metoda spustí v jiném vlákně"

To není pravda, metoda se zavolá naprosto standardně a co dělá je čistě na ní, klíčové slovo await s voláním metod nijak nesouvisí, viz "Synchronně se pokračuje prováděním akcí před await v těle metody RunComputeAndShowResults" tj. metoda se zavolala. Naopak klíčové slovo await se uvádí před výraz, který vrací (obecně tzv. awaitable) objekt, kterým je nejčastěji instance třídy Task.

např. tento kód jde proto přepsat na:

Task t = RunComputeAndShowResults();
await t;

"Stejně sis tím await zastavil běh hlavního vlákna"

To také není pravda, await hlavní ani jiné vlákno nikdy neblokuje, jak v článku uvádím "Hlavní vlákno není v tomto okamžiku ničím blokováno" (navíc tučným písmem). V momentě prvního await na objektu, kterým je v daný moment nedokončený Task (jinak by se jednalo o tzv. "fast path"), běh metody končí a pouze nastaví callback na celý zbytek kódu (jako u tzv. CPS - Continuation Passing Style).

Příště si zkuste článek opravdu přečíst a trochu se nad tím zamyslet.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

diki

dakujem tento článok mi veľmi pomohol lepšie pochopiť ako to celé funguje

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Poděkování

Děkuji, tento článek mi hodně pomohl pochopit, jak to vnitřně funguje...

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Pěkný článek

Pěkný článek, díky moc. Hodně mi pomohl (napotřetí). :-)

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback