V moderním vývoji softwaru, zejména v oblasti webových aplikací a paralelního zpracování dat, je asynchronní programování klíčovým nástrojem pro optimalizaci výkonu a efektivního využití systémových prostředků. Zvláště v prostředí .NET Frameworku je tato technika nezbytná pro vývoj výkonných aplikací, které dokážou efektivně reagovat na požadavky uživatelů při minimalizaci zátěže serverů.

Příkladem tohoto přístupu je blok JoinBlock, který umožňuje spojení více vstupních toků do jediného výstupního toku v podobě n-tic. Tento princip je součástí širšího konceptu zpracování dat v podobě potrubí (pipeline), což znamená, že bloky provádějí paralelní zpracování dat, ale každý blok obvykle zpracovává jednu zprávu najednou. Tento způsob práce je efektivní, pokud všechny bloky trvají podobnou dobu zpracování, avšak pokud je některý z bloků pomalejší, je možné konfigurací bloků ActionBlock a TransformBlock aktivovat paralelní zpracování uvnitř jednotlivých bloků. Tímto způsobem se každý blok rozdělí na více identických podsystémů, které sdílejí zátěž.

Ve spojení s Task Parallel Library (TPL) přichází vylepšení díky asynchronním metodám, které umožňují delegátům ActionBlock a TransformBlock pracovat asynchronně. To je obzvlášť důležité při interakcích s dlouho běžícími vzdálenými operacemi, které mohou běžet paralelně, aniž by došlo k zbytečnému blokování vláken. Pro snadnější interakci s datovými bloky zvenčí je užitečné použít asynchronní přístup, například metodu SendAsync, která je součástí rozhraní ITargetBlock a zjednodušuje proces odesílání dat do asynchronních bloků.

Pokud se podíváme na testování asynchronního kódu, můžeme narazit na několik výzev. Asynchronní metody vracejí okamžitě objekt Task, který je dokončen v budoucnu. To znamená, že testovací rámce, jako je MSTest, mohou chybně označit testy za úspěšné, pokud se neřeší správně čekání na dokončení těchto metod. Jedním ze způsobů, jak se vyhnout těmto problémům, je vyhnout se označování testovacích metod jako asynchronních a místo toho počkat na výsledek synchronně, což však přináší riziko zablokování vlákna.

Efektivní způsob testování asynchronního kódu spočívá v použití testovacích rámců, které nativně podporují asynchronní metody, jako jsou xUnit.net nebo MSTest. Tyto rámce automaticky čekají na dokončení úkolu, než označí test za úspěšný, čímž usnadňují psaní testů pro asynchronní kód a minimalizují možnost chybných výsledků.

Ve světě webových aplikací .NET Frameworku je asynchronní programování obzvlášť cenné. Webový server měří svou výkonnost především podle propustnosti a latence, což znamená, že asynchronní kód může výrazně snížit spotřebu vláken a tím i paměťové nároky serveru. To je důležité zejména na přetížených serverech, kde každý vlákno zabírá značnou paměť, a jakmile paměť dojde, je nutné často spouštět garbage collector, což může zpomalit aplikaci.

V prostředí ASP.NET je podpora asynchronních operací od verze 2.0, ale až s příchodem C# 5 a .NET 4.5 je implementace asynchronních metod v aplikacích tak snadná, že je vhodné ji využívat. V ASP.NET MVC 4 a novějších verzích je možné snadno implementovat asynchronní akce pomocí modelu Task-based Asynchronous Pattern (TAP), což zjednodušuje práci s dlouhotrvajícími operacemi jako jsou databázové dotazy. Stačí, když metoda kontroleru vrátí Task, a uživatelské rozhraní je schopno efektivně reagovat bez zbytečného blokování vláken.

Pokud používáte starší verze ASP.NET MVC, implementace asynchronního kódu je složitější a vyžaduje použití třídy AsyncController místo běžného Controller. Tento model zahrnuje dvě metody pro každou akci, jednu pro asynchronní operaci a druhou pro její dokončení. I když je tento přístup poněkud složitější, umožňuje využít asynchronní programování v těchto starších verzích.

Pokud čtenář zvažuje implementaci asynchronního kódu ve své aplikaci, měl by mít na paměti nejen technické výhody, ale také výzvy, které mohou nastat při testování a implementaci. Pochopení toho, jak efektivně používat asynchronní metody a správně s nimi testovat, může výrazně zlepšit kvalitu aplikace a uživatelský zážitek.

Kde v C# nemůžeme použít await a proč na tom záleží

Klíčovou součástí asynchronního programování v C# je koncept synchronizačního kontextu. Ten zajišťuje, že asynchronní metoda může být obnovena ve správném vlákně – například ve vlákně UI, které má přístup ke grafickým prvkům. V prostředí .NET se tento kontext zachycuje a přenáší pomocí třídy ExecutionContext, která tvoří rodičovský rámec pro další specifické kontexty: SecurityContext a CallContext. Každý z nich nese určité informace, jež se běžně vážou k vláknu, jako je bezpečnostní identita nebo uživatelsky definovaná metadata.

Asynchronní model v .NET však narušuje předpoklad, že vlákno zůstává konstantní během celého běhu metody. Vlákno je po dobu čekání uvolněno, a metoda může být později obnovena na jiném vlákně. Proto tradiční mechanismy jako thread-local storage přestávají fungovat. Pokud tedy potřebujeme zachovat kontext mezi částmi kódu, které běží na různých vláknech, musíme použít CallContext, případně jeho variantu LogicalCallContext, schopnou fungovat i přes hranice AppDomain.

Přenos těchto kontextů má však výkonnostní dopady. Například použití impersonace (běh kódu jako jiný uživatel) v kombinaci s rozsáhlým využitím async/await může výrazně zpomalit běh aplikace. Pokud tedy nepotřebujete specifické kontexty, je vhodné se jim vyhýbat.

Přestože await lze použít téměř kdekoliv v metodě označené klíčovým slovem async, existují důležitá omezení, která jsou dána logikou jazyka C# a transformací, kterou kompilátor nad kódem provádí.

Použití await není dovoleno uvnitř catch a finally bloků. Důvod je technický: během exekuce těchto bloků stále probíhá tzv. unwinding zásobníku výjimek. Vložení await by narušilo tento proces – metoda by byla pozastavena a obnovena na jiném místě zásobníku, což by způsobilo nekonzistenci v chování výjimek. Pokud je potřeba reagovat na výjimku asynchronně, doporučuje se uchovat výsledek operace do proměnné a await volat mimo catch.

Také použití await uvnitř lock bloků není platné. Důvodem je, že lock zajišťuje exkluzivní přístup k určitému objektu pouze po dobu běhu v rámci jednoho vlákna. Pokud by metoda mezi await a obnovením změnila vlákno, zámek by nebyl zachován. Alternativou je rozdělení operace do dvou oddělených lock bloků: jeden před asynchronní operací, druhý po jejím dokončení. V případech, kde je třeba zachovat zámek přes asynchronní operaci, je nutné přehodnotit návrh programu, protože vzniká riziko vzniku deadlocků a nekonzistentních stavů.

Další omezení platí pro LINQ dotazové výrazy. Většina výrazů v LINQ je překládána do anonymních funkcí (lambda výrazů), a protože jazyk neumožňuje označit tyto implicitní lambdy jako async, nelze do nich umístit await. Řešením je přepsání výrazu do explicitního tvaru s použitím rozšiřujících metod, kde lze lambda výrazy jasně označit jako asynchronní.

Kód označený jako unsafe nesmí obsahovat await. Unsafe kód pracuje na úrovni přímé manipulace s pamětí a nemůže být bezpečně přerušen a obnoven, jak to await dělá. Proto musí zůstat synchronní a izolovaný.

Zachytávání výjimek v asynchronních metodách funguje podobně jako ve standardním synchronním kódu, ale s jedním zásadním rozdílem. Výjimka není okamžitě vyhozena, ale je zabalena do objektu Task, který je návratovou hodnotou metody. Pokud volající použije await, výjimka je opět vyhozena v místě,