Aszinkron programozási forgatókönyvek
Ha I/O-kötött igényei vannak (például adatok lekérése egy hálózatról, adatbázis elérése, vagy írás és olvasás egy fájlrendszerbe), akkor aszinkron programozást kell használnia. A processzorhoz kötött kóddal is rendelkezhet, például költséges számításokat végezhet, ami szintén jó forgatókönyv az aszinkron kód írásához.
A C# nyelvszintű aszinkron programozási modellel rendelkezik, amely lehetővé teszi az aszinkron kód egyszerű írását anélkül, hogy visszahívásokat kellene váltania, vagy meg kell felelnie az aszinkronitást támogató kódtárnak. Az úgynevezett feladatalapú aszinkron mintát (TAP) követi.
Az aszinkron modell áttekintése
Az aszinkron programozás lényege az és Task<T>
az Task
objektumok, amelyek aszinkron műveleteket modelleznek. Ezeket a kulcsszavak és await
a async
kulcsszavak támogatják. A modell a legtöbb esetben meglehetősen egyszerű:
- I/O-kötött kód esetén olyan műveletet vár, amely egy metódus egy
Task
vagyTask<T>
azonasync
belüli részét adja vissza. - Cpu-kötött kód esetén egy olyan műveletet vár, amely egy háttérszálon indul el a Task.Run metódussal.
A await
kulcsszó az, ahol a varázslat történik. Lehetővé teszi az elvégzett metódus hívójának irányítását await
, és végső soron lehetővé teszi, hogy a felhasználói felület rugalmas legyen, vagy egy szolgáltatás rugalmas legyen. Bár az aszinkron kód async
await
megközelítésének különböző módjai vannak, ez a cikk a nyelvi szintű szerkezetekre összpontosít.
Feljegyzés
Az alábbi példák System.Net.Http.HttpClient némelyikében az osztály egy webszolgáltatásból tölt le néhány adatot.
Az s_httpClient
ezekben a példákban használt objektum egy statikus osztálymező Program
(ellenőrizze a teljes példát):
private static readonly HttpClient s_httpClient = new();
I/O-kötött példa: Adatok letöltése webszolgáltatásból
Előfordulhat, hogy egy gomb megnyomásakor le kell töltenie néhány adatot egy webszolgáltatásból, de nem szeretné letiltani a felhasználói felületi szálat. A következő módon valósítható meg:
s_downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await s_httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
A kód a szándékot fejezi ki (az adatok aszinkron módon való letöltését) anélkül, hogy le kellene ásni az objektumokkal Task
való interakció során.
CPU-kötött példa: Számítás végrehajtása egy játékhoz
Tegyük fel, hogy egy mobiljátékot ír, ahol egy gomb lenyomásával sok ellenség megsérülhet a képernyőn. A kárszámítás végrehajtása költséges lehet, és a felhasználói felületen végzett művelet szünetelteti a játékot a számítás végrehajtása közben!
Ennek kezelésére a legjobb módszer egy háttérszál indítása, amely a munkát végzi Task.Run
, és várja az eredményt a használatával await
. Ez lehetővé teszi, hogy a felhasználói felület zökkenőmentesen érezze magát a munka során.
static DamageResult CalculateDamageDone()
{
return new DamageResult()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
};
}
s_calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
Ez a kód egyértelműen kifejezi a gomb kattintási eseményének szándékát, nincs szükség háttérszál manuális kezelésére, és ezt nem blokkoló módon teszi.
Mi történik a borítók alatt?
A C# oldalon a fordító állapotgépgé alakítja a kódot, amely nyomon követi az olyan dolgokat, mint például a végrehajtás elérésekor await
, illetve a végrehajtás folytatása egy háttérfeladat befejezésekor.
Elméletileg ez az aszinkron ígéretmodell implementációja.
A megértéshez fontos részek
- Az aszinkron kód használható I/O- és CPU-kötött kódokhoz is, de minden forgatókönyv esetében eltérően.
- Az aszinkron kód a háttérben végzett munka modellezéséhez használt szerkezeteket
Task
és szerkezeteket használjaTask<T>
. - A
async
kulcsszó egy metódust aszinkron metódussá alakít, amely lehetővé teszi a kulcsszó használatát aawait
törzsében. - A
await
kulcsszó alkalmazásakor felfüggeszti a hívási metódust, és a várt feladat befejezéséig visszavesz a hívónak. await
csak aszinkron metóduson belül használható.
Cpu- és I/O-kötésű munka felismerése
Az útmutató első két példája bemutatta, hogyan használhatja async
az I/O-kötött és await
a CPU-hoz kötött munkát. Fontos, hogy megállapítsa, mikor kell elvégeznie egy feladatot I/O- vagy CPU-kötéssel, mert ez nagyban befolyásolhatja a kód teljesítményét, és bizonyos szerkezetek helytelen használatához vezethet.
Az alábbiakban két kérdést kell feltennie, mielőtt bármilyen kódot ír:
A kód "várakozik" valamire, például egy adatbázisból származó adatokra?
Ha a válasza "igen", akkor a munkája I/O-kötött.
A kód költséges számítást hajt végre?
Ha igennel válaszolt, akkor a munkája processzorhoz kötött.
Ha a munka I/O-kötött, használja async
és await
anélkülTask.Run
. Ne használja a párhuzamos feladattárat.
Ha a munka van cpu-kötött, és érdekli a válaszkészség, a használat async
és await
, de ív le a munkát egy másik szálon. Task.Run
Ha a munka megfelel az egyidejűségnek és a párhuzamosságnak, fontolja meg a párhuzamos feladattár használatát is.
Emellett mindig mérnie kell a kód végrehajtását. Előfordulhat például, hogy olyan helyzetben találja magát, amikor a processzorhoz kötött munka nem elég költséges a többszálas környezetkapcsolók terheléséhez képest. Minden választásnak megvan a maga kompromisszuma, és a helyzetének megfelelő kompromisszumot kell választania.
További példák
Az alábbi példák az aszinkron kód C#-ban való írásának különböző módjait mutatják be. Ezek néhány különböző forgatókönyvet fednek le.
Adatok kinyerve egy hálózatból
Ez a kódrészlet letölti a HTML-t a megadott URL-címről, és megszámolja, hogy a ".NET" sztring hányszor fordul elő a HTML-ben. A ASP.NET használatával definiál egy web API-vezérlőmetódust, amely végrehajtja ezt a feladatot, és visszaadja a számot.
Feljegyzés
Ha html-elemzést tervez éles kódban, ne használjon normál kifejezéseket. Ehelyett használjon elemzési kódtárat.
[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
// Suspends GetDotNetCount() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await s_httpClient.GetStringAsync(URL);
return Regex.Matches(html, @"\.NET").Count;
}
Ugyanez a forgatókönyv egy univerzális Windows-alkalmazáshoz is készült, amely ugyanazt a feladatot hajtja végre, amikor egy gombot lenyom:
private readonly HttpClient _httpClient = new HttpClient();
private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
// Capture the task handle here so we can await the background task later.
var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");
// Any other work on the UI thread can be done here, such as enabling a Progress Bar.
// This is important to do here, before the "await" call, so that the user
// sees the progress bar before execution of this method is yielded.
NetworkProgressBar.IsEnabled = true;
NetworkProgressBar.Visibility = Visibility.Visible;
// The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.
// This is what allows the app to be responsive and not block the UI thread.
var html = await getDotNetFoundationHtmlTask;
int count = Regex.Matches(html, @"\.NET").Count;
DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";
NetworkProgressBar.IsEnabled = false;
NetworkProgressBar.Visibility = Visibility.Collapsed;
}
Várjon, amíg több tevékenység befejeződik
Előfordulhat, hogy olyan helyzetben találja magát, amikor egyszerre több adatrészt kell lekérnie. Az Task
API két metódust tartalmaz, Task.WhenAll és Task.WhenAnylehetővé teszi aszinkron kód írását, amely nem blokkoló várakozást hajt végre több háttérfeladaton.
Ez a példa bemutatja, hogyan foghatja fel User
az adatokat egy s-halmazhoz userId
.
private static async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
return await Task.FromResult(new User() { id = userId });
}
private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}
return await Task.WhenAll(getUserTasks);
}
A LINQ használatával a következő módon írhatja tömörebben a következőt:
private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
Bár ez kevésbé kód, óvatosan keverje a LINQ-t az aszinkron kóddal. Mivel a LINQ késleltetett (lusta) végrehajtást használ, az aszinkron hívások nem történnek azonnal, ahogy azok egy foreach
hurokban történnek, hacsak nem kényszeríti a generált sorozat iterálását egy hívással .ToList()
vagy .ToArray()
. A fenti példa arra használ, Enumerable.ToArray hogy lelkesen hajtsa végre a lekérdezést, és tárolja az eredményeket egy tömbben. Ez arra kényszeríti a kódot id => GetUserAsync(id)
, hogy futtassa és indítsa el a feladatot.
Fontos információk és tanácsok
Az aszinkron programozással néhány részletet szem előtt kell tartani, amelyek megakadályozhatják a váratlan viselkedést.
async
metódusoknak rendelkezniük kell egyawait
kulcsszó a testükben, vagy soha nem fog hozamot!Ezt fontos szem előtt tartani. Ha
await
nem egy metódus törzsébenasync
használják, a C#-fordító figyelmeztetést hoz létre, de a kód lefordítja és úgy fut, mintha normál módszer lenne. Ez hihetetlenül nem hatékony, mivel az aszinkron metódus C#-fordítója által létrehozott állapotgép nem hajt végre semmit.Adja hozzá az "Async" nevet az összes írott aszinkron metódusnév utótagjaként.
Ez az a konvenció, amelyet a .NET-ben használnak a szinkron és aszinkron metódusok egyszerűbb megkülönböztetésére. Bizonyos, a kód által nem explicit módon meghívott metódusok (például eseménykezelők vagy webvezérlő-metódusok) nem feltétlenül érvényesek. Mivel a kód nem kifejezetten hívja meg őket, az elnevezésük nem olyan fontos.
async void
csak eseménykezelőkhöz használható.async void
Az aszinkron eseménykezelők csak azért használhatók, mert az események nem rendelkeznek visszatérésiTask
típusokkal (így nem használhatók ésTask<T>
nem használhatók). Az egyéb használatasync void
nem követi a TAP-modellt, és nehéz lehet használni, például:- A metódusban
async void
szereplő kivételeket nem lehet az adott metóduson kívül elkapni. async void
módszereket nehéz tesztelni.async void
a metódusok rossz mellékhatásokat okozhatnak, ha a hívó nem várja, hogy aszinkron legyen.
- A metódusban
Aszinkron lambdák LINQ-kifejezésekben való használatakor körültekintően járjon el
A LINQ Lambda-kifejezései késleltetett végrehajtást használnak, ami azt jelenti, hogy a kód végrehajtása olyan időpontban történhet, amikor nem számít rá. A blokkolási feladatok bevezetése könnyen holtpontot eredményezhet, ha nem megfelelően van megírva. Emellett az ilyen aszinkron kód beágyazása is megnehezítheti a kód végrehajtásának okát. Az Async és a LINQ hatékonyak, de a lehető legtiszteletesen és legtisztánosabban kell együtt használni.
Olyan kód írása, amely nem blokkoló módon várja a feladatokat
Ha az aktuális szálat úgy blokkolja, hogy megvárja a
Task
befejezést, holtpontokhoz és blokkolt környezeti szálakhoz vezethet, és összetettebb hibakezelést igényelhet. Az alábbi táblázat útmutatást nyújt a feladatok várakozásának blokkolás nélküli kezelésére:Használandó karakterlánc Ahelyett, hogy ez... Ha ezt szeretné tenni... await
Task.Wait
vagyTask.Result
Háttérfeladat eredményének lekérése await Task.WhenAny
Task.WaitAny
Várakozás a tevékenységek befejezésére await Task.WhenAll
Task.WaitAll
Várakozás az összes tevékenység befejezésére await Task.Delay
Thread.Sleep
Várakozás egy időre Fontolja meg a lehetőség szerinti használatot
ValueTask
Az objektumok aszinkron metódusokból való visszaadása
Task
teljesítménybeli szűk keresztmetszeteket okozhat bizonyos útvonalakon.Task
egy referenciatípus, ezért használata egy objektum kiosztását jelenti. Azokban az esetekben, amikor aasync
módosítóval deklarált metódus gyorsítótárazott eredményt ad vissza, vagy szinkron módon fejeződik be, a további foglalások jelentős időköltséget jelenthetnek a kód kritikus fontosságú szakaszaiban. Költségessé válhat, ha ezek a kiosztások szoros hurkokban történnek. További információ: általános aszinkron visszatérési típusok.Fontolja meg a használatot
ConfigureAwait(false)
Gyakori kérdés, hogy "mikor érdemes használni a Task.ConfigureAwait(Boolean) módszert?". A metódus lehetővé teszi, hogy egy
Task
példány konfigurálja a váróját. Ez egy fontos szempont, és helytelenül történő beállítása potenciálisan hatással lehet a teljesítményre, és akár holtpontokra is. További információ:ConfigureAwait
ConfigureAwait FAQ.Kevesebb állapotalapú kód írása
Nem függhet a globális objektumok állapotától vagy bizonyos metódusok végrehajtásától. Ehelyett csak a metódusok visszatérési értékeitől függ. Miért?
- A kóddal könnyebb lesz érvelni.
- A kód könnyebben tesztelhető lesz.
- Az aszinkron és szinkron kód keverése sokkal egyszerűbb.
- A versenyfeltételek általában teljesen elkerülhetők.
- A visszatérési értékektől függően az aszinkron kód koordinálása egyszerűvé válik.
- (Bónusz) nagyon jól működik a függőséginjektálással.
Ajánlott cél a teljes vagy majdnem teljes hivatkozási átláthatóság elérése a kódban. Ez kiszámítható, tesztelhető és karbantartható kódbázist eredményez.
Példa kitöltése
Az alábbi kód a példához tartozó Program.cs fájl teljes szövege.
using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;
class Button
{
public Func<object, object, Task>? Clicked
{
get;
internal set;
}
}
class DamageResult
{
public int Damage
{
get { return 0; }
}
}
class User
{
public bool isEnabled
{
get;
set;
}
public int id
{
get;
set;
}
}
public class Program
{
private static readonly Button s_downloadButton = new();
private static readonly Button s_calculateButton = new();
private static readonly HttpClient s_httpClient = new();
private static readonly IEnumerable<string> s_urlList = new string[]
{
"https://learn.microsoft.com",
"https://learn.microsoft.com/aspnet/core",
"https://learn.microsoft.com/azure",
"https://learn.microsoft.com/azure/devops",
"https://learn.microsoft.com/dotnet",
"https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
"https://learn.microsoft.com/education",
"https://learn.microsoft.com/shows/net-core-101/what-is-net",
"https://learn.microsoft.com/enterprise-mobility-security",
"https://learn.microsoft.com/gaming",
"https://learn.microsoft.com/graph",
"https://learn.microsoft.com/microsoft-365",
"https://learn.microsoft.com/office",
"https://learn.microsoft.com/powershell",
"https://learn.microsoft.com/sql",
"https://learn.microsoft.com/surface",
"https://dotnetfoundation.org",
"https://learn.microsoft.com/visualstudio",
"https://learn.microsoft.com/windows",
"https://learn.microsoft.com/maui"
};
private static void Calculate()
{
// <PerformGameCalculation>
static DamageResult CalculateDamageDone()
{
return new DamageResult()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
};
}
s_calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
// </PerformGameCalculation>
}
private static void DisplayDamage(DamageResult damage)
{
Console.WriteLine(damage.Damage);
}
private static void Download(string URL)
{
// <UnblockingDownload>
s_downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await s_httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
// </UnblockingDownload>
}
private static void DoSomethingWithData(object stringData)
{
Console.WriteLine("Displaying data: ", stringData);
}
// <GetUsersForDataset>
private static async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
return await Task.FromResult(new User() { id = userId });
}
private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}
return await Task.WhenAll(getUserTasks);
}
// </GetUsersForDataset>
// <GetUsersForDatasetByLINQ>
private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
// </GetUsersForDatasetByLINQ>
// <ExtractDataFromNetwork>
[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
// Suspends GetDotNetCount() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await s_httpClient.GetStringAsync(URL);
return Regex.Matches(html, @"\.NET").Count;
}
// </ExtractDataFromNetwork>
static async Task Main()
{
Console.WriteLine("Application started.");
Console.WriteLine("Counting '.NET' phrase in websites...");
int total = 0;
foreach (string url in s_urlList)
{
var result = await GetDotNetCount(url);
Console.WriteLine($"{url}: {result}");
total += result;
}
Console.WriteLine("Total: " + total);
Console.WriteLine("Retrieving User objects with list of IDs...");
IEnumerable<int> ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
var users = await GetUsersAsync(ids);
foreach (User? user in users)
{
Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
}
Console.WriteLine("Application ending.");
}
}
// Example output:
//
// Application started.
// Counting '.NET' phrase in websites...
// https://learn.microsoft.com: 0
// https://learn.microsoft.com/aspnet/core: 57
// https://learn.microsoft.com/azure: 1
// https://learn.microsoft.com/azure/devops: 2
// https://learn.microsoft.com/dotnet: 83
// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31
// https://learn.microsoft.com/education: 0
// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42
// https://learn.microsoft.com/enterprise-mobility-security: 0
// https://learn.microsoft.com/gaming: 0
// https://learn.microsoft.com/graph: 0
// https://learn.microsoft.com/microsoft-365: 0
// https://learn.microsoft.com/office: 0
// https://learn.microsoft.com/powershell: 0
// https://learn.microsoft.com/sql: 0
// https://learn.microsoft.com/surface: 0
// https://dotnetfoundation.org: 16
// https://learn.microsoft.com/visualstudio: 0
// https://learn.microsoft.com/windows: 0
// https://learn.microsoft.com/maui: 6
// Total: 238
// Retrieving User objects with list of IDs...
// 1: isEnabled= False
// 2: isEnabled= False
// 3: isEnabled= False
// 4: isEnabled= False
// 5: isEnabled= False
// 6: isEnabled= False
// 7: isEnabled= False
// 8: isEnabled= False
// 9: isEnabled= False
// 0: isEnabled= False
// Application ending.