Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Model asynchronního programování úloh (TAP) poskytuje abstraktní vrstvu nad typickým asynchronním kódováním. V tomto modelu napíšete kód jako posloupnost příkazů, stejně jako obvykle. Rozdíl je, že kód založený na úlohách si můžete přečíst, protože kompilátor zpracovává každý příkaz a před zahájením zpracování dalšího příkazu. Aby bylo možné tento model provést, kompilátor provádí mnoho transformací, aby dokončil každou úlohu. Některé příkazy mohou zahájit práci a vrátit objekt Task, který představuje probíhající práci a kompilátor musí tyto transformace vyřešit. Cílem asynchronního programování úkolů je umožnit kód, který čte jako posloupnost příkazů, ale provádí se v složitějším pořadí. Provádění je založeno na přidělování externích zdrojů a po dokončení úkolů.
Model asynchronního programování úkolů je podobný tomu, jak lidé poskytují pokyny pro procesy, které zahrnují asynchronní úlohy. Tento článek používá příklad s pokyny k přípravě snídaně, aby ukázal, jak klíčová slova async a await usnadňují uvažování o kódu, který obsahuje řadu asynchronních instrukcí. Pokyny k vytvoření snídaně mohou být uvedeny jako seznam:
- Nalít šálek kávy.
- Zahřejte pánev, pak smažte dvě vejce.
- Připravte tři bramborové placky hash brown.
- Opečte dva plátky chleba.
- Rozložte máslo a džem na toast.
- Nalít sklenici pomerančové šťávy.
Pokud máte zkušenosti s vařením, můžete tyto pokyny dokončit asynchronně. Začnete hřejit pánev pro vejce a pak začnete vařit hash browns. Chleba vložíte do toustovače a pak začnete vařit vejce. V každém kroku procesu spustíte úkol a pak přejdete na jiné úkoly, které jsou připravené pro vaši pozornost.
Vaření snídaně je dobrým příkladem asynchronní práce, která není paralelní. Jedna osoba (nebo vlákno) může zpracovávat všechny úkoly. Jedna osoba může snídani asynchronně provést spuštěním dalšího úkolu před dokončením předchozího úkolu. Každý úkol vaření postupuje bez ohledu na to, jestli někdo aktivně sleduje proces. Jakmile začnete hřejit pánev pro vejce, můžete začít vařit hash browns. Poté, co se hashbrowns začnou smažit, můžete dát chléb do toustovače.
Pro paralelní algoritmus potřebujete více lidí, kteří uvaří (nebo více vláken). Jeden člověk připraví vejce, druhý připraví bramborové placky, a tak dále. Každá osoba se zaměřuje na svůj konkrétní úkol. Každá osoba, která vaří (nebo každé vlákno), je synchronně blokována a čeká na dokončení aktuálního úkolu: Hash browns jsou připraveny k otočení, chleba připraven k vyskočení z toustru a tak dále.
Představte si stejný seznam synchronních instrukcí, které jsou napsané jako příkazy kódu jazyka C#:
using System;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class HashBrown { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static void Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = FryEggs(2);
Console.WriteLine("eggs are ready");
HashBrown hashBrown = FryHashBrowns(3);
Console.WriteLine("hash browns are ready");
Toast toast = ToastBread(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static Toast ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
Task.Delay(3000).Wait();
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static HashBrown FryHashBrowns(int patties)
{
Console.WriteLine($"putting {patties} hash brown patties in the pan");
Console.WriteLine("cooking first side of hash browns...");
Task.Delay(3000).Wait();
for (int patty = 0; patty < patties; patty++)
{
Console.WriteLine("flipping a hash brown patty");
}
Console.WriteLine("cooking the second side of hash browns...");
Task.Delay(3000).Wait();
Console.WriteLine("Put hash browns on plate");
return new HashBrown();
}
private static Egg FryEggs(int howMany)
{
Console.WriteLine("Warming the egg pan...");
Task.Delay(3000).Wait();
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
Task.Delay(3000).Wait();
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
Pokud tyto pokyny interpretujete jako počítač, snídaně trvá asi 30 minut, než se připraví. Doba trvání je součet jednotlivých časů úkolů. Počítač blokuje každý příkaz, dokud není veškerá práce dokončena, a poté pokračuje na další úkol. Tento přístup může trvat výrazně dlouho. V příkladu snídaně vytvoří počítačová metoda neuspokojivou snídani. Pozdější úkoly v synchronním seznamu, jako je opékání chleba, se nespustí, dokud nebudou dřívější úkoly dokončeny. Některé jídlo je studené, než je snídaně připravena k obsluhě.
Pokud chcete, aby počítač prováděl instrukce asynchronně, musíte napsat asynchronní kód. Při psaní klientských programů chcete, aby uživatelské rozhraní reagovalo na uživatelský vstup. Při stahování dat z webu by vaše aplikace neměla ukotvit veškerou interakci. Při psaní serverových programů nechcete blokovat vlákna, která by mohla obsluhovat jiné požadavky. Použití synchronního kódu v případech, kdy existují asynchronní alternativy, snižuje vaše schopnost škálovat kapacitu méně nákladně. Platíte za blokovaná vlákna.
Úspěšné moderní aplikace vyžadují asynchronní kód. Bez podpory jazyka vyžaduje psaní asynchronního kódu zpětná volání, události dokončení nebo jiné prostředky, které zakrývají původní záměr kódu. Výhodou synchronního kódu je postupný způsob, který usnadňuje skenování a pochopit. Tradiční asynchronní modely vás přinutí zaměřit se na asynchronní povahu kódu, ne na základní akce kódu.
Neblokujte, místo toho čekejte
Předchozí kód zdůrazňuje nešťastný programovací postup: Psaní synchronního kódu pro provádění asynchronních operací. Kód blokuje aktuální vlákno, aby provádělo jakoukoli jinou práci. Kód nepřeruší vlákno v době, kdy jsou spuštěné úlohy. Výsledek tohoto modelu se podobá zírání na toustovač po vložení chleba. Ignorujete všechna přerušení a nezačínáte další úkoly, dokud chleba nevyskočí. Nevezmeš si máslo a marmeládu z chladničky. Můžete přehlédnout, že na sporáku začíná hořet. Chcete si přichystnout chleba a současně zvládnout další obavy. Totéž platí pro váš kód.
Můžete začít aktualizací kódu, aby vlákno neblokoval, když jsou spuštěné úlohy. Klíčové slovo await poskytuje odblokující způsob spuštění úkolu a po dokončení úkolu pokračujte v provádění. Jednoduchá asynchronní verze kódu snídaně vypadá jako následující fragment kódu:
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = await FryEggsAsync(2);
Console.WriteLine("eggs are ready");
HashBrown hashBrown = await FryHashBrownsAsync(3);
Console.WriteLine("hash browns are ready");
Toast toast = await ToastBreadAsync(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
Kód aktualizuje původní těla metod FryEggs, FryHashBrowns a ToastBread tak, aby vracely objekty Task<Egg>, Task<HashBrown> a Task<Toast> v uvedeném pořadí. Aktualizované názvy metod zahrnují příponu "Async": FryEggsAsync, FryHashBrownsAsynca ToastBreadAsync. Metoda Main vrátí objekt Task, i když nemá výraz return, což je záměr. Pro více informací si přečtěte Vyhodnocení asynchronní funkce vracející void.
Poznámka
Aktualizovaný kód zatím nevyužívá klíčové funkce asynchronního programování, což může vést k kratší době dokončení. Kód zpracovává úlohy přibližně ve stejné době jako počáteční synchronní verze. Úplné implementace metod najdete v konečné verzi kódu dále v tomto článku.
Pojďme na aktualizovaný kód použít příklad snídaně. Vlákno neblokuje, zatímco vejce nebo hash browns jsou vaření, ale kód také nespustí další úkoly, dokud se aktuální práce nedokončí. Stále dáváte chléb do toustovače a díváte se na toustovač, dokud chleba nevyskočí, ale nyní můžete reagovat na vyrušení. V restauraci, kde se podává více objednávek, může kuchař začít novou objednávku, zatímco jiný už vaření.
V aktualizovaném kódu není vlákno pracující na snídani blokováno při čekání na jakýkoli spuštěný úkol, který není dokončen. U některých aplikací je tato změna vše, co potřebujete. Aplikaci můžete povolit, aby podporovala interakci uživatelů při stahování dat z webu. V jiných scénářích můžete chtít spustit jiné úkoly při čekání na dokončení předchozího úkolu.
Souběžné spouštění úloh
U většiny operací chcete okamžitě spustit několik nezávislých úloh. Po dokončení každého úkolu zahájíte další práci, která je připravená začít. Když použijete tuto metodologii na příklad snídaně, můžete si snídani připravit rychleji. Získáte také všechno připravené blízko ke stejné době, takže si můžete vychutnat horkou snídani.
Třídy System.Threading.Tasks.Task a související typy jsou třídy, které můžete použít k použití tohoto stylu odůvodnění u probíhajících úkolů. Tento přístup umožňuje psát kód, který se více podobá způsobu, jakým vytváříte snídani v reálném životě. Začnete současně připravovat vejce, bramboráky a toast. Vzhledem k tomu, že každá položka jídla vyžaduje akci, obrátíte pozornost na tento úkol, postaráte se o danou akci a pak počkáte na něco jiného, co vyžaduje vaši pozornost.
Ve vašem kódu spustíte úlohu a podržíte objekt Task, který zastupuje tuto úlohu. Metodu await použijete ke zpoždění zahájení práce na úkolu, dokud nebude výsledek hotov.
Tyto změny použijte u kódu snídaně. Prvním krokem je uložení úloh pro operace při jejich spuštění místo použití výrazu await:
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Console.WriteLine("Breakfast is ready!");
Tyto revize nepomáhají připravit snídani rychleji. Výraz await se použije u všech úkolů hned po spuštění. Dalším krokem je přesunutí await výrazů pro hash browns a vejce na konec metody před podáváním snídaně.
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");
Console.WriteLine("Breakfast is ready!");
Teď máte asynchronně připravenou snídani, která trvá asi 20 minut, než se připraví. Celková doba vaření je snížena, protože některé úlohy běží souběžně.
Kód aktualizuje proces přípravy snížením doby vaření, ale zavádí regresi vypalováním vajec a hash hnědých. Spustíte všechny asynchronní úlohy najednou. Na každý úkol čekáte jenom v případě, že potřebujete výsledky. Kód může být podobný programu ve webové aplikaci, která odesílá požadavky na různé mikroslužby a pak kombinuje výsledky do jedné stránky. Všechny požadavky provedete okamžitě a potom použijete výraz await na všechny tyto úkoly a vytvoříte webovou stránku.
Kompozice úkolů
Předchozí revize kódu pomáhají připravit všechno na snídani ve stejnou dobu, kromě toastu. Proces vytváření toastu je složení asynchronní operace (opékání chleba) se synchronními operacemi (namazání másla a džemu na toast). Tento příklad ukazuje důležitý koncept asynchronního programování:
Důležitý
Složení asynchronní operace následované synchronní prací je asynchronní operace. Uvedli jsme jiný způsob, pokud je některá část operace asynchronní, je celá operace asynchronní.
V předchozích aktualizacích jste zjistili, jak používat Task nebo Task<TResult> objekty k uložení spuštěných úloh. Než použijete jeho výsledek, počkáte na každý úkol. Dalším krokem je vytvoření metod, které představují kombinaci jiné práce. Než podáte snídani, chcete počkat na úkol, který představuje opékání chleba, než na něj natřete máslo a marmeládu.
Tuto práci můžete vyjádřit následujícím kódem:
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
Metoda MakeToastWithButterAndJamAsync má v podpisu modifikátor async, který signalizuje kompilátoru, že metoda obsahuje výraz await a obsahuje asynchronní operace. Metoda představuje úkol, který opéká chleba, poté roztírá máslo a džem. Metoda vrátí Task<TResult> objekt, který představuje složení tří operací.
Upravený hlavní blok kódu teď vypadá takto:
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
var eggsTask = FryEggsAsync(2);
var hashBrownTask = FryHashBrownsAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var eggs = await eggsTask;
Console.WriteLine("eggs are ready");
var hashBrown = await hashBrownTask;
Console.WriteLine("hash browns are ready");
var toast = await toastTask;
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
Tato změna kódu znázorňuje důležitou techniku pro práci s asynchronním kódem. Úkoly vytvoříte oddělením operací do nové metody, která vrací úkol. Můžete zvolit, kdy na tento úkol počkat. Další úlohy můžete spouštět souběžně.
Zpracování asynchronních výjimek
Do tohoto okamžiku váš kód implicitně předpokládá, že všechny úkoly byly úspěšně dokončeny. Asynchronní metody vyvolává výjimky, stejně jako jejich synchronní protějšky. Cíle asynchronní podpory pro výjimky a zpracování chyb jsou stejné jako pro asynchronní podporu obecně. Osvědčeným postupem je napsat kód, který čte jako řadu synchronních příkazů. Úkoly vyvolají výjimky, když se nemohou úspěšně dokončit. Klientský kód může tyto výjimky zachytit při použití výrazu await na spuštěnou úlohu.
V příkladu se snídaní předpokládejme, že se toustovač vznítí při opékání chleba. Tento problém můžete simulovat tak, že upravíte metodu ToastBreadAsync tak, aby odpovídala následujícímu kódu:
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(2000);
Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");
await Task.Delay(1000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
Poznámka
Při kompilaci tohoto kódu se zobrazí upozornění na nedostupný kód. Tato chyba je navržená. Jakmile toustovač zachytí oheň, operace nepokračuje normálně a kód vrátí chybu.
Po provedení změn kódu spusťte aplikaci a zkontrolujte výstup:
Pouring coffee
Coffee is ready
Warming the egg pan...
putting 3 hash brown patties in the pan
Cooking first side of hash browns...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a hash brown patty
Flipping a hash brown patty
Flipping a hash brown patty
Cooking the second side of hash browns...
Cracking 2 eggs
Cooking the eggs ...
Put hash browns on plate
Put eggs on plate
Eggs are ready
Hash browns are ready
Unhandled exception. System.InvalidOperationException: The toaster is on fire
at AsyncBreakfast.Program.ToastBreadAsync(Int32 slices) in Program.cs:line 65
at AsyncBreakfast.Program.MakeToastWithButterAndJamAsync(Int32 number) in Program.cs:line 36
at AsyncBreakfast.Program.Main(String[] args) in Program.cs:line 24
at AsyncBreakfast.Program.<Main>(String[] args)
Všimněte si, že poměrně mnoho úkolů se dokončí mezi okamžikem, kdy toustovač začne hořet, a chvílí, kdy systém zjistí výjimku. Pokud úloha, která běží asynchronně vyvolá výjimku, je tento úkol chybný. Objekt Task obsahuje výjimku vyvolanou ve vlastnosti Task.Exception. Chybné úlohy vyvolají výjimku při použití výrazu await na úlohu.
Existují dva důležité mechanismy pro pochopení tohoto procesu:
- Jak je výjimka uložená v chybné úloze
- Jak se výjimka dekomprimuje a znovu vyvolá, když kód čeká (
await) na chybném úkolu
Pokud kód spuštěný asynchronně vyvolá výjimku, je výjimka uložena v Task objektu. Vlastnost Task.Exception je objekt System.AggregateException, protože během asynchronní práce může být vyvolány více než jedna výjimka. Jakákoli vyvolaná výjimka se přidá do kolekce AggregateException.InnerExceptions. Pokud je vlastnost Exception null, vytvoří se nový objekt AggregateException a vyvolaná výjimka se stane první položkou v kolekci.
Nejběžnějším scénářem chybné úlohy je, že vlastnost Exception obsahuje přesně jednu výjimku. Když váš kód čeká na chybnou úlohu, znovu vyvolá první AggregateException.InnerExceptions výjimku v kolekci. Tento výsledek je důvodem, proč výstup z příkladu zobrazuje objekt System.InvalidOperationException místo objektu AggregateException. Extrahování první vnitřní výjimky činí práci s asynchronními metodami co nejpodobnější práci s jejich synchronními protějšky. Vlastnost Exception v kódu můžete prozkoumat, když váš scénář může vygenerovat více výjimek.
Rada
Doporučeným postupem je, aby všechny výjimky ověřování argumentů vznikly synchronně z metod vracejících úlohy. Další informace a příklady naleznete v tématu Výjimky v metodách vracejících úlohy.
Než budete pokračovat k další části, zakomentujte následující dva příkazy ve své metodě ToastBreadAsync. Nechcete spustit další oheň:
Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");
Efektivní použití výrazů await u úkolů
Řadu výrazů await můžete vylepšit na konci předchozího kódu pomocí metod Task třídy. Jedno rozhraní API je metoda WhenAll, která vrací objekt Task, který se dokončí, když jsou dokončeny všechny úkoly v seznamu argumentů. Následující kód ukazuje tuto metodu:
await Task.WhenAll(eggsTask, hashBrownTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Hash browns are ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");
Další možností je použít metodu WhenAny, která vrátí Task<Task> objekt, který se dokončí při dokončení některého z jejích argumentů. Na vrácený úkol můžete počkat, protože víte, že se úkol dokončil. Následující kód ukazuje, jak můžete pomocí metody WhenAny počkat na dokončení prvního úkolu a pak zpracovat jeho výsledek. Po zpracování výsledku z dokončeného úkolu odeberete dokončený úkol ze seznamu úkolů předaných do WhenAny metody.
var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("Eggs are ready");
}
else if (finishedTask == hashBrownTask)
{
Console.WriteLine("Hash browns are ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("Toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Na konci fragmentu kódu si všimněte výrazu await finishedTask;. Tento řádek je důležitý, protože Task.WhenAny vrací Task<Task> - obalovou úlohu, která obsahuje dokončenou úlohu. Když await Task.WhenAnyčekáte na dokončení obalového úkolu, výsledkem je skutečná úloha, která se dokončí jako první. Pokud ale chcete načíst výsledek úkolu nebo zajistit, aby byly všechny výjimky správně vyvolány, musíte await samotný dokončený úkol (uložený v finishedTask). I když víte, že úkol byl dokončen, čekání na jeho dokončení vám umožní získat přístup k jeho výsledku nebo zpracovat všechny výjimky, které mohly způsobit chybu.
Kontrola konečného kódu
Finální verze kódu vypadá takto:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class HashBrown { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
var eggsTask = FryEggsAsync(2);
var hashBrownTask = FryHashBrownsAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("eggs are ready");
}
else if (finishedTask == hashBrownTask)
{
Console.WriteLine("hash browns are ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(3000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static async Task<HashBrown> FryHashBrownsAsync(int patties)
{
Console.WriteLine($"putting {patties} hash brown patties in the pan");
Console.WriteLine("cooking first side of hash browns...");
await Task.Delay(3000);
for (int patty = 0; patty < patties; patty++)
{
Console.WriteLine("flipping a hash brown patty");
}
Console.WriteLine("cooking the second side of hash browns...");
await Task.Delay(3000);
Console.WriteLine("Put hash browns on plate");
return new HashBrown();
}
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
Kód dokončí asynchronní úlohy související se snídaní přibližně za 15 minut. Celková doba se sníží, protože některé úlohy běží souběžně. Kód současně monitoruje více úloh a provádí akce pouze podle potřeby.
Konečný kód je asynchronní. Přesněji odráží, jak může člověk vařit snídani. Porovnejte konečný kód s první ukázkou kódu v článku. Základní akce jsou stále jasné čtením kódu. Konečný kód si můžete přečíst stejným způsobem jako v seznamu pokynů pro vytvoření snídaně, jak je znázorněno na začátku článku. Jazykové funkce pro klíčová slova async a await pomáhají při překladu, který každý člověk provádí podle psaných pokynů: Zahajte úkoly, jakmile můžete, a nezůstávejte zablokovaní, zatímco čekáte na dokončení úkolů.
Async/await vs ContinueWith
Klíčová slova async a await poskytují syntaktické zjednodušení oproti přímému použití Task.ContinueWith. Zatímco async/await a ContinueWith mají podobnou sémantiku pro zpracování asynchronních operací, kompilátor nemusí nutně překládat await výrazy přímo do ContinueWith volání metody. Místo toho kompilátor generuje optimalizovaný kód stavového počítače, který poskytuje stejné logické chování. Tato transformace poskytuje významné výhody čitelnosti a udržovatelnosti, zejména při řetězení více asynchronních operací.
Představte si scénář, ve kterém potřebujete provádět několik sekvenčních asynchronních operací. Tady je, jak vypadá stejná logika při implementaci pomocí ContinueWith oproti async/await.
Použití funkce ContinueWith
Každý ContinueWithkrok v posloupnosti asynchronních operací vyžaduje vnořené pokračování:
// Using ContinueWith - demonstrates the complexity when chaining operations
static Task MakeBreakfastWithContinueWith()
{
return StartCookingEggsAsync()
.ContinueWith(eggsTask =>
{
var eggs = eggsTask.Result;
Console.WriteLine("Eggs ready, starting bacon...");
return StartCookingBaconAsync();
})
.Unwrap()
.ContinueWith(baconTask =>
{
var bacon = baconTask.Result;
Console.WriteLine("Bacon ready, starting toast...");
return StartToastingBreadAsync();
})
.Unwrap()
.ContinueWith(toastTask =>
{
var toast = toastTask.Result;
Console.WriteLine("Toast ready, applying butter...");
return ApplyButterAsync(toast);
})
.Unwrap()
.ContinueWith(butteredToastTask =>
{
var butteredToast = butteredToastTask.Result;
Console.WriteLine("Butter applied, applying jam...");
return ApplyJamAsync(butteredToast);
})
.Unwrap()
.ContinueWith(finalToastTask =>
{
var finalToast = finalToastTask.Result;
Console.WriteLine("Breakfast completed with ContinueWith!");
});
}
Použití async/await
Stejná posloupnost operací, které používají async/await, působí mnohem přirozeněji:
// Using async/await - much cleaner and easier to read
static async Task MakeBreakfastWithAsyncAwait()
{
var eggs = await StartCookingEggsAsync();
Console.WriteLine("Eggs ready, starting bacon...");
var bacon = await StartCookingBaconAsync();
Console.WriteLine("Bacon ready, starting toast...");
var toast = await StartToastingBreadAsync();
Console.WriteLine("Toast ready, applying butter...");
var butteredToast = await ApplyButterAsync(toast);
Console.WriteLine("Butter applied, applying jam...");
var finalToast = await ApplyJamAsync(butteredToast);
Console.WriteLine("Breakfast completed with async/await!");
}
Proč se preferuje async/await
Přístup async/await nabízí několik výhod:
- Čitelnost: Kód čte jako synchronní kód, což usnadňuje pochopení toku operací.
- Udržovatelnost: Přidání nebo odebrání kroků v sekvenci vyžaduje minimální změny kódu.
-
Zpracování chyb: Zpracování výjimek s
try/catchbloky funguje přirozeně, zatímcoContinueWithvyžaduje pečlivé zpracování chybných úloh. -
Ladění: Prostředí zásobníku volání a ladicího programu je mnohem lepší s
async/await. -
Výkon: Optimalizace kompilátoru jsou
async/awaitpropracovanější než ručně vytvořené řetězce.
Výhoda se stává ještě patrnější, když se zvyšuje počet zřetězených operací. I když může být jedno pokračování spravovatelné pomocí ContinueWith, sekvence 3-4 nebo více asynchronních operací se rychle stávají obtížně čitelnými a udržovatelnými. Tento model, označovaný jako "monadic do-notation" v funkčním programování, umožňuje vytvářet více asynchronních operací sekvenčním a čitelným způsobem.