Jedním z klíčových rozdílů mezi asynchronním a synchronním kódem je okamžik, kdy je vyhozena výjimka z volané metody. V asynchronních metodách se výjimka neprojeví ihned při samotném volání metody, ale až při operátoru await, tedy v místě, kde se skutečně čeká na dokončení asynchronní operace. Tento rozdíl je zásadní pro správné zacházení s výjimkami, protože snadno vede k situacím, kdy se zapomene await použít, a výjimka zůstane nepozorována. Takový přístup je ekvivalentní prázdnému bloku catch, který zachytí všechny výjimky a ignoruje je, což může způsobit neplatný stav programu a velmi složité chyby, které se projeví až daleko od místa vzniku problému.

V praxi je proto nezbytné vždy awaitovat všechny asynchronní metody, i ty vracející negenerický Task, kde nepotřebujeme žádnou návratovou hodnotu. Toto chování představuje změnu oproti předchozím verzím .NET, kde výjimky z kódu Task Parallel Library mohly být znovu vyhozeny na finální vlákno, což nyní v .NET 4.5 a novějších verzích neplatí.

U asynchronních metod vracejících void se situace komplikuje, protože tyto metody nelze awaitovat. Výjimky, které z takových metod uniknou, jsou znovu vyhozeny na volající vlákno: pokud existuje SynchronizationContext, výjimka je na něj „postnuta“, jinak je vyhozena na vlákně z thread poolu. V obou případech to většinou vede k ukončení procesu, pokud není připojen handler pro neobsloužené výjimky. Proto je vhodné psát async void metody pouze tehdy, pokud jsou určeny pro volání z externího kódu, například event handlery, nebo pokud je zaručeno, že nevyhodí výjimku.

Existují i případy tzv. „fire and forget“, kdy výsledek metody nezajímá a await by komplikoval kód. V takových situacích je dobré vracet Task a předat jej speciální metodě, která ošetří výjimky, například pomocí ContinueWith, kde se výjimky logují, aby se zabránilo jejich ztrátě a nevědomému ignorování.

Asynchronní svět přináší i situace, které v synchronním světě neexistovaly – například možnost, že metoda vyhodí najednou více výjimek, což se stává například při použití Task.WhenAll k čekání na skupinu asynchronních úloh. Task místo jedné výjimky obsahuje AggregateException, která sdružuje všechny jednotlivé výjimky. Ačkoli await v případě vyhození AggregateException znovu vyhodí pouze první vnořenou výjimku, je možné po zachycení této výjimky přímo z Task získat celý seznam vnitřních výjimek.

Podle TAP (Task-based Asynchronous Pattern) mohou metody vyhodit výjimky synchronně, ale pouze pokud jde o chybu v parametrech nebo jiné chyby volání metody, nikoli chyby vzniklé při samotném běhu asynchronní operace. Prakticky to znamená, že je možné mít synchroní kontrolu parametrů ještě před voláním asynchronní metody, čímž lze usnadnit diagnostiku a získat čitelnější zásobník volání.

Použití try...finally v asynchronních metodách je povoleno a funguje tak, jak by se dalo očekávat – blok finally se spustí vždy, když metoda opouští try blok, a to bez ohledu na to, zda byl odchytán výjimka, nebo jestli byla metoda dokončena normálně. Ovšem existuje skrytá komplikace: protože asynchronní metoda může být pozastavena na await a nikdy více se neprobudit (například pokud je Task opuštěn a zanikne), nemusí být finally nikdy vykonán. Tato vlastnost ukazuje na nutnost důkladného návrhu kódu a správného řízení životního cyklu asynchronních operací.

Je důležité chápat, že asynchronní výjimky vyžadují jiné způsoby zacházení než ty synchronní, a opomíjení await může vést k závažným chybám, které se obtížně diagnostikují. Správné čekání na úlohy a ošetření výjimek je základem spolehlivého asynchronního kódu. Také je třeba rozlišovat situace, kdy je vhodné použít async void versus Task a kdy je vhodné implementovat metody pro bezpečné ignorování výsledků s logováním výjimek.

Významnou oblastí, kterou nelze přehlédnout, je také souběžné spouštění více asynchronních úloh a jejich agregace výjimek, která představuje zcela nový model práce s chybami. Pro vývojáře to znamená potřebu důkladně analyzovat všechny možné scénáře selhání a správně zpracovat všechny výjimky v kolekci, nikoli pouze první.

Proč je výkon asynchronního kódu tak důležitý a jak jej skutečně pochopit?

Výkon asynchronního kódu nelze pochopit bez srovnání s jeho synchronními alternativami. Při rozhodování o použití async a await nejde pouze o styl zápisu, ale o reálný dopad na výkon aplikace, její odezvu, propustnost a využití systémových prostředků. Ať už jde o uživatelské rozhraní, serverový kód nebo architekturu založenou na aktorech, vždy je třeba posoudit, zda přínos asynchronního modelu skutečně převáží jeho náklady.

Každé volání asynchronní metody s sebou nese režii – více cyklů procesoru, vyšší latenci způsobenou přepínáním vláken, komplexnější správu kontextu. Zatímco blokující kód je jednodušší a méně náročný na CPU, zadržuje vlákna a spotřebovává paměť, což může být kritickým problémem na serverech s vysokým zatížením. Naopak asynchronní kód umožňuje lepší škálování za cenu zvýšeného výpočetního overheadu. Rozhodující otázka tedy nezní, který přístup je obecně lepší, ale který přístup je vhodný v dané architektuře a konkrétním scénáři.

Visual Studio poskytuje pozoruhodně propracovanou podporu pro ladění asynchronního kódu. I když samotný Intellitrace nezobrazuje vždy přesně původní názvy metod, okno zásobníku volání (Call Stack) dokáže identifikovat transformace provedené kompilátorem a přeložit vygenerované konstrukty, jako MoveNext, zpět na původní metody. Klíčem k pochopení chování běhového systému je možnost krokovat (Step Over, Step Out) přes await, a sledovat, jak a kdy se metoda obnovuje na jiném vlákně.

Z hlediska výkonnosti hraje zásadní roli SynchronizationContext. Právě ten určuje, zda bude po await potřeba přepnout vlákno (Post), což je operace s vysokou režií. Pokud například metoda await Task.Yield() běží pod jiným kontextem než ten, ve kterém byla volána, runtime vynutí přepnutí vlákna. Testy ukazují, že náklady tohoto přepnutí se liší: ve Windows Forms nebo WPF aplikacích může být až desetkrát náročnější než běžné volání metody.

Je-li vlákno, které dokončí Task, jiné než původní, .NET se snaží obnovit metodu v původním kontextu. Pokud je to možné, provede obnovení synchronně. Pokud to však kontext neumožňuje, využije vlákno z poolu, nebo explicitně zavolá Post, čímž dochází k výraznému nárůstu nákladů. Právě tyto přepínače mezi vlákny bývají nejdražší – a právě jim se lze v některých případech vyhnout.

V kontextu uživatelského rozhraní je použití async téměř nevyhnutelné. Pokud operace trvá více než několik milisekund, blokování hlavního UI vlákna by vedlo k neakceptovatelnému zpoždění a špatné uživatelské zkušenosti. Cena jednoho přepnutí vlákna (Post) je zanedbatelná ve srovnání se zpožděním způsobeným síťovým voláním trvajícím stovky milisekund.

WPF je v tomto ohledu výjimečné – často vytváří nové instance SynchronizationContext, což vede k tomu, že každé obnovení asynchronní metody vyžaduje nový a drahý Post. V jiných prostředích, jako je Windows Forms nebo běžné konzolové aplikace, je tento problém méně výrazný.

Na straně serveru je rozhodování komplexnější. Zásadní otázkou je, zda server trpí na nedostatek paměti kvůli velkému počtu vláken. Pokud ano, asynchronní kód představuje řešení. I když spotřebuje více CPU, uvolní paměť, protože asynchronní operace neblokují vlákna. Pokud server není zatížen procesorově, ale paměťově, pak async kód umožňuje zvýšit propustnost bez nutnosti navyšování prostředků.

Optimalizace asynchronního kódu začíná u metody ConfigureAwait(false). Tato volba říká runtime, že obnovení metody po await nemusí proběhnout v původním SynchronizationContext, čímž se výrazně snižuje nákladnost operace. V aplikacích bez uživatelského rozhraní nebo v kódu, kde není nutné obnovit tok řízení v původním vlákně, je tato optimalizace téměř nezbytná.

Asynchronní metody vždy spotřebují více procesorového času než jejich synchronní protějšky. Rozdíl je ale ve většině případů zanedbatelný ve srovnání s dalšími aspekty aplikace, jako je síťová latence nebo přístup k disku. Kritickým faktorem je především správná architektonická volba: blokující operace tam, kde je třeba okamžité odpovědi, a asynchronní přístup tam, kde škálovatelnost a efektivita převažují.

Významnou roli v této rovnováze hraje i chování .NET thread poolu. Jeho efektivita je tak vysoká, že většina přepnutí na něj se ani neprojeví v měřitelných overheadech. Pokud tedy chybí SynchronizationContext, nebo je výchozí, metoda se často obnoví synchronně na tomtéž vlákně – bez nákladného přepnutí. To zjednodušuje některé optimalizační rozhodnutí a zároveň umožňuje dosažení dobrého výkonu i bez složitého ladění.

Je důležité si uvědomit, že výběr mezi synchronní a asynchronní implementací není binární. Jde o průběžné vyhodnocování prostředí, architektury, metrik paměti a procesoru, a přizpůsobování se konkrétnímu typu aplikace – ať už jde o klientskou aplikaci s uživatelským rozhraním, nebo vysoce výkonný serverový backend.

Jak a proč psát asynchronní metody v C#

Transformace metody AddAFavicon do asynchronní podoby nám umožňuje, aby uživatelské rozhraní zůstalo citlivé i během síťových operací. Použití klíčových slov async a await není jen stylistickým vylepšením – jde o zásadní změnu chování aplikace. Asynchronní metoda nezačíná nutně jako asynchronní – spustí se synchronně až do prvního await, kde se výpočet pozastaví a předá řízení zpět volajícímu vláknu, zpravidla hlavnímu vláknu UI.

Metoda AddAFavicon ilustruje základní vzor asynchronního stahování: vytvoří se WebClient, zavolá se DownloadDataTaskAsync a výsledek se použije ke konstrukci UI prvku. Při pohledu na kód je patrné, že jeho struktura zůstává téměř totožná se synchronní verzí. Přesto chování odpovídá pokročilejšímu manuálnímu zpracování asynchronního kódu pomocí callbacků. Výhodou je, že čitelnost a údržba kódu jsou výrazně lepší, zatímco výkonnost aplikace zůstává optimalizovaná pro responsivní chování.

Použití Task a await poskytuje vyšší úroveň abstrakce nad dřívějšími modely práce s asynchronními operacemi, jako byly například delegáty nebo explicitní callbacky. Objekt Task reprezentuje probíhající operaci. Pokud má výsledek, jde o Task<T>, jinak pouze o Task. Při aplikaci await na Task<T> výraz získává typ T, tedy návratový typ operace. Naproti tomu await aplikovaný na Task (bez generického typu) se chová jako příkaz – nelze jej přiřadit k proměnné, protože nevrací žádnou hodnotu.

Zásadní je pochopit, že samotné zavolání metody jako DownloadStringTaskAsync(uri) operaci okamžitě spouští, ještě před await. Tím je možné spustit více operací paralelně, uložit jejich Tasky, a následně je awaitovat. Například:

csharp
Task firstTask = webClient1.DownloadStringTaskAsync("http://oreilly.com");
Task secondTask = webClient2.DownloadStringTaskAsync("http://simple-talk.com"); string firstPage = await firstTask; string secondPage = await secondTask;

Tento přístup ale nese riziko. Pokud více úloh vyvolá výjimku, první await zvedne svou výjimku a druhý Task může zůstat nezpracovaný. Jeho výjimka pak není nikdy zachycena, což může způsobit nestabilitu aplikace. Proto je vhodné použít například Task.WhenAll, který shromáždí výjimky a dovolí jejich kontrolované zpracování.

Návratové typy asynchronních metod v C# jsou omezené na void, Task a Task<T>. Jiné typy nejsou povoleny, protože asynchronní metoda nedává výsledek hned. async void se používá výjimečně, typicky v obsluze událostí (např. UI), kde není možné čekat na výsledek. Jde o tzv. "fire-and-forget" operace. Naproti tomu Task dovoluje volajícímu čekat na dokončení a zachytit případné výjimky. Pokud je zapotřebí vrátit výsledek, použije se Task<T>.

Přestože async vypadá jako součást signatury metody, není z pohledu překladače součástí jejího rozhraní. To znamená, že lze přepsat virtuální metodu nebo implementovat rozhraní bez nutnosti použití async v nadtřídě nebo rozhraní. Klíčové slovo async mění pouze způsob, jakým je metoda přeložena, nikoliv jak je volána nebo jak se chová zvenčí. Například:

csharp
class BaseClass {
public virtual async Task AlexsMethod() { ... }
}
class SubClass : BaseClass { public override async Task AlexsMethod() { ... } }

Zvenčí není patrné, zda metoda používá async, dokud se neprozkoumá její tělo. Tato vlastnost zaručuje zpětnou kompatibilitu a volnost v návrhu knihoven, které chtějí přidat asynchronitu bez narušení veřejného API.

Při práci s asynchronním kódem je důležité dodržovat několik zásad: nikdy nepoužívejte async void, pokud to není nutné, vždy obalte výjimky a mějte pod kontrolou paralelní spouštění úloh. Následná správa těchto úloh je klíčem ke stabilní a robustní aplikaci. Asynchronní programování je silný nástroj, ale pouze tehdy, je-li používán uvědoměle a s pochopením jeho modelu běhu.

Dále je nutné chápat širší důsledky zavádění asynchronity. I když samotné přidání async a await zlepšuje chování aplikace, může vést k novým třídám chyb, jako jsou zapomenuté výjimky, „zombie“ úlohy nebo zacyklení. Větší systémové aplikace by měly být navrženy s ohledem na správu životního cyklu Tasků, synchronizaci (např. pomocí SemaphoreSlim) a důslednou správu prostředků.

Jak vlastně funguje klíčové slovo await a proč je async „nakažlivé“?

Klíčové slovo await v jazyce C# není jen syntaktickým cukrem; je hluboce integrováno do celého asynchronního modelu jazyka. Když v metodě označené jako async narazíme na await, dochází k přerušení běhu metody, uvolnění vlákna a pozdějšímu obnovení stavu metody až po dokončení očekávané úlohy. Tento mechanismus umožňuje asynchronní chování bez explicitního použití vlákna nebo vlákna pozadí.

Pro běžného vývojáře je důležité pochopit, že samotné rozhraní (interface) nemůže určovat, zda se metoda implementuje jako async. Rozhraní může pouze předepsat návratový typ – například Task – ale nezajímá ho, zda metoda uvnitř používá await. To je čistě záležitostí implementace. Rozhraní se zaměřuje na kontrakt, nikoli na způsob jeho naplnění.

Návratový typ v async metodách se chová specificky. I když metoda vrací Task<T>, výraz v return musí být typu T, nikoli Task<T>. Kompilátor se postará o zabalování výstupu do úlohy (Task) za vás. Například při return page.Length; uvnitř async Task<int> metody se hodnota automaticky zabalí do Task<int>.

U metod označených jako async existuje také specifická pravidla pro používání return. Pokud metoda vrací void nebo Task, příkaz return; je volitelný. Naopak, pokud metoda vrací Task<T>, pak každý kódový tok musí skončit příkazem return s výrazem typu T.

Asynchronní programování má tendenci šířit se napříč kódem. Jednou zavedené async metody vyžadují, aby jejich volající rovněž používaly await a tudíž i async. Tímto způsobem vznikají řetězce metod, kde každá volá jinou asynchronní metodu a sama se musí stát asynchronní. Tento jev je běžně označován jako „nakažlivost“ async. V praxi to však nepředstavuje problém – jazyk C# dělá zápis asynchronních metod tak snadným, že přirozené rozšíření asynchronity je spíše výhodou než překážkou.

Anonymní delegáty a lambda výrazy mohou být také asynchronní. Syntaxe je téměř totožná s běžnými metodami. Například:

csharp
Func<Task<int>> getNumberAsync = async delegate { return 3; }; Func<Task<string>> getWordAsync = async () => "hello";

Stejná pravidla, která platí pro klasické async metody, se vztahují i na tyto anonymní formy. Mohou zachycovat proměnné z okolního kontextu a zachovávají stručnost a čitelnost.

Když kód narazí na await, dojde k tzv. hibernaci metody. Stav metody – tedy hodnoty všech lokálních proměnných, parametrů, proměnných ve smyčkách, a dokonce i objekt this – je uložen do speciálního objektu na haldě. Vlákno, které metodu provádělo, je uvolněno a metoda formálně skončí. Až se očekávaná úloha (Task) dokončí, metoda pokračuje přesně tam, kde skončila, jako by nikdy nepřestala běžet.

Analogicky si to lze představit jako režim spánku u počítače: await je jako hibernace (S4), kde je vše uloženo na disk a zařízení lze klidně odpojit od napájení. Oproti tomu synchronní blokující metody připomínají režim spánku (S3), kde stále běží procesy a jsou obsazeny systémové prostředky.

Během čekání (await) C# udržuje nejen hodnoty proměnných, ale také přesné místo v kódu, kde se metoda nachází. Může jít i o složené výrazy, například:

csharp
int myNum = await AlexsMethodAsync(await myTask, await StuffAsync());

V tomto případě musí být mezivýsledky uchovány na zásobníku a každý dílčí await správně navázán. Interní reprezentace v IL (Intermediate Language) používá zásobník pro tyto části výrazů, a kompilátor odpovídá za správné obnovení všech potřebných stavů.

Když se dosáhne prvního await, metoda okamžitě vrací Task. Tento objekt slouží jako zástupce běhu metody a umožňuje volajícímu zaregistrovat pokračování, až metoda skončí. Celý tento mechanismus je velmi přesně řízen kompilátorem, a vývojář má iluzi, že metoda nikdy nepřestala běžet.

Dále se během await ukládá tzv. kontext – například synchronizační kontext UI vlákna – který se po dokončení obnovuje. Tento mechanismus zajišťuje, že obs