Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
Ha a kód I/O-kötött forgatókönyveket implementál a hálózati adatkérelmek, az adatbázis-hozzáférés vagy a fájlrendszer olvasási/írási műveleteinek támogatására, a legjobb módszer az aszinkron programozás. Aszinkron kódot is írhat processzorhoz kötött forgatókönyvekhez, például költséges számításokhoz.
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. A modell az úgynevezett tevékenységalapú aszinkron mintát (TAP)követi.
Az aszinkron programozási modell megismerése
A Task
és Task<T>
objektumok az aszinkron programozás magját képviselik. Ezek az objektumok aszinkron műveletek modellezésére szolgálnak a async
és await
kulcsszavak támogatásával. A modell a legtöbb esetben meglehetősen egyszerű mind az I/O-, mind a CPU-kötött forgatókönyvekhez. Egy async
metóduson belül:
-
I/O-kötött kód elindít egy műveletet, amelyet egy
Task
vagyTask<T>
objektum jelöl aasync
metóduson belül. - processzorhoz kötött kód elindít egy műveletet egy háttérszálon a Task.Run metódussal.
Az aktív Task
mindkét esetben olyan aszinkron műveletet jelöl, amely esetleg nem fejeződik be.
A await
kulcsszó az, ahol a varázslat történik. Lehetővé teszi a await
kifejezést tartalmazó metódus hívójának irányítását, é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 megközelíteni az aszinkron kódot, ez a cikk a nyelvi szintű szerkezetekre összpontosít.
Feljegyzés
A cikkben bemutatott néhány példa a System.Net.Http.HttpClient osztály használatával tölt le adatokat egy webszolgáltatásból. A példakódban a s_httpClient
objektum Program
osztály típusú statikus mező:
private static readonly HttpClient s_httpClient = new();
Alapfogalmak áttekintése
Amikor Aszinkron programozást implementál a C#-kódban, a fordító állapotgéppé alakítja a programot. Ez a szerkezet nyomon követi a kód különböző műveleteit és állapotát, például végrehajtást eredményez, amikor a kód elér egy await
kifejezést, és végrehajtja a végrehajtást, amikor egy háttérfeladat befejeződik.
Ami a számítástechnika elméletét illeti, az aszinkron programozás az aszinkron Promise modellimplementációja.
Az aszinkron programozási modellben több alapvető fogalmat is meg kell érteni:
- Használhat aszinkron kódot az I/O-kötött és a CPU-kötött kódhoz is, de a megvalósítás eltérő.
- Az aszinkron kód
Task<T>
ésTask
objektumokat használ szerkezetként a háttérben futó munka modellezéséhez. - A
async
kulcsszó aszinkron metódusként deklarál egy metódust, amely lehetővé teszi aawait
kulcsszó használatát a metódus törzsében. - A
await
kulcsszó alkalmazásakor a kód felfüggeszti a hívó metódust, és visszaadja a vezérlést a hívónak, amíg a feladat befejeződik. - A
await
kifejezést csak aszinkron metódusban használhatja.
I/O-kötött példa: Adatok letöltése webszolgáltatásból
Ebben a példában, amikor a felhasználó kiválaszt egy gombot, az alkalmazás letölti az adatokat egy webszolgáltatásból. A letöltési folyamat során nem szeretné letiltani az alkalmazás felhasználói felületi szálát. A következő kód végzi el ezt a feladatot:
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 kifejezi a szándékot (az adatok aszinkron letöltését) anélkül, hogy elmélyedne a Task
objektumokkal való interakcióban.
CPU-kötött példa: Játékszámítás futtatása
A következő példában egy mobiljáték több ügynöknek okoz kárt a képernyőn egy gombeseményre válaszul. A kárszámítás elvégzése költséges lehet. A számítás felhasználói felületi szálon való futtatása megjelenítési és felhasználói felületi interakciós problémákat okozhat a számítás során.
A feladat kezelésének legjobb módja, ha elindít egy háttérszálat a Task.Run
metódussal történő munka befejezéséhez. A művelet egy await
kifejezéssel hajtható végre. A művelet a feladat befejeződésekor folytatódik. Ez a megközelítés lehetővé teszi, hogy a felhasználói felület zökkenőmentesen fusson, miközben a munka a háttérben befejeződik.
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);
};
A kód egyértelműen kifejezi a gomb Clicked
esemény szándékát. Nincs szükség háttérszálak kézi kezelésére, és nem blokkoló módon végzi el a feladatot.
Cpu- és I/O-kötött forgatókönyvek felismerése
Az előző példák bemutatják, hogyan használható a async
módosító és await
kifejezés az I/O-kötött és a CPU-kötött munka esetében. Az egyes forgatókönyvek példái bemutatják, hogyan különbözik a kód attól függően, hogy hol van kötve a művelet. A megvalósításra való felkészüléshez tisztában kell lenni azzal, hogyan állapítható meg, hogy egy művelet I/O-kötéssel vagy CPU-kötéssel van-e kötve. A megvalósítási döntés nagyban befolyásolhatja a kód teljesítményét, és esetleg a szerkezetek helytelen használatához vezethet.
A kód írása előtt két elsődleges kérdést kell megválaszolni:
Kérdés | Forgatókönyv | Megvalósítás |
---|---|---|
Várjon a kód egy eredményre vagy műveletre, például egy adatbázisból származó adatokra? | I/O-kötött | A Kerülje a párhuzamos feladattár használatát. |
A kódnak költséges számítást kell futtatnia? | processzorhoz kötött | Használja a async módosítót és a await kifejezést, de a Task.Run metódussal ossza meg a munkát egy másik szálon. Ez a megközelítés a processzor válaszképességével kapcsolatos problémákat kezeli. Ha a munka megfelel az egyidejűségnek és a párhuzamosságnak, fontolja meg a párhuzamos feladattár használatát is. |
Mindig mérje a kód végrehajtását. Előfordulhat, hogy a processzorhoz kötött munka nem elég költséges a kontextusváltások miatti többletterheléssel szemben, amikor többszálú feldolgozást alkalmaz. Minden választásnak vannak kompromisszumai. Válassza ki a helyzetének megfelelő kompromisszumot.
További példák felfedezése
Az ebben a szakaszban szereplő példák számos módszert mutatnak be az aszinkron kód C#-ban való írására. Néhány lehetséges forgatókönyvet fednek le.
Adatok kinyerése egy hálózatból
Az alábbi kód letölti a HTML-t egy adott URL-címről, és megszámolja, hogy a ".NET" sztring hányszor fordul elő a HTML-ben. A kód ASP.NET használ egy webes API-vezérlő metódus definiálásához, amely végrehajtja a feladatot, és visszaadja a darabszá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;
}
Egy univerzális Windows-alkalmazáshoz hasonló kódot írhat, és gombnyomás után végrehajthatja a számlálási feladatot:
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.
// It's important to do the extra work here before the "await" call,
// so 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 action 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
Bizonyos esetekben a kódnak egyszerre több adatrészt kell lekérnie. A Task
API-k olyan metódusokat biztosítanak, amelyekkel aszinkron kódot írhat, amely nem tiltó várakozást hajt végre több háttérfeladaton:
- Task.WhenAll módszer
- Task.WhenAny módszer
Az alábbi példa bemutatja, hogyan ragadhatja meg User
objektumadatokat userId
objektumok egy csoportjához.
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);
}
Ezt a kódot tömörebben is megírhatja a LINQ használatával:
private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
Bár kevesebb kódot ír a LINQ használatával, körültekintően járjon el a LINQ aszinkron kóddal való összekeverésekor. A LINQ késleltetett (vagy lusta) végrehajtást használ. Az aszinkron hívások nem történnek meg azonnal, mint egy foreach
ciklusban, hacsak nem kényszeríti a generált sorozat iterálását a .ToList()
vagy .ToArray()
metódusok hívásával. Ez a példa a Enumerable.ToArray metódust használja a lekérdezés lelkes végrehajtásához és az eredmények tömbben való tárolásához. Ez a megközelítés arra kényszeríti a id => GetUserAsync(id)
utasítást, hogy futtassa és indítsa el a feladatot.
Az aszinkron programozás szempontjainak áttekintése
Az aszinkron programozással számos olyan részletet figyelembe kell venni, amely megakadályozhatja a váratlan viselkedést.
A await inside async() metódus törzsének használata
A async
módosító használatakor egy vagy több await
kifejezést kell tartalmaznia a metódus törzsében. Ha a fordító nem talál await
kifejezést, a metódus nem tud eredményt adni. Bár a fordító figyelmeztetést generál, a kód továbbra is lefordul, és a fordító végrehajtja a metódust. Az aszinkron metódus C#-fordítója által létrehozott állapotgép nem hajt végre semmit, ezért a teljes folyamat nem hatékony.
"Async" utótag hozzáadása aszinkron metódusnevekhez
A .NET stíluskonvenciája, hogy az "Async" utótagot hozzáadja az összes aszinkron metódusnévhez. Ez a megközelítés segít könnyebben megkülönböztetni a szinkron és az aszinkron metódusokat. Ebben a forgatókönyvben 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 ezeket az elemeket a kód nem kifejezetten hívja meg, az explicit elnevezés használata nem olyan fontos.
Csak eseménykezelőktől származó "aszinkron void" értéket ad vissza
Az eseménykezelőknek deklarálniuk kell void
visszatérési típusokat, és nem használhatnak és nem adhatnak vissza Task
és Task<T>
objektumokat, mint más metódusok. Aszinkron eseménykezelők írásakor a async
módosítóját egy void
visszatérési metóduson kell használnia a kezelők számára. A async void
visszatérési metódusok egyéb implementációi nem követik a TAP-modellt, és kihívást jelenthetnek:
- A
async void
metódusban alkalmazott kivételek nem ragadhatók ki a metóduson kívül -
async void
módszereket nehéz tesztelni -
async void
metódusok negatív mellékhatásokat okozhatnak, ha a hívó nem számít az aszinkronra
Óvatosan használja az aszinkron lambdákat a LINQ-ban
A LINQ-kifejezésekben az aszinkron lambdák implementálásakor fontos körültekintően eljárni. A LINQ Lambda-kifejezései késleltetett végrehajtást használnak, ami azt jelenti, hogy a kód váratlan időpontban végrehajtható. A blokkolási feladatok ebben a forgatókönyvben való bevezetése könnyen holtponthoz vezethet, ha a kód nem megfelelően van megírva. Emellett az 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 ezeket a technikákat a lehető leg körültekintően és egyértelműen kell használni.
Feladatok hozzáadása nem blokkoló módon
Ha a programnak szüksége van egy feladat eredményére, írjon olyan kódot, amely nem tiltó módon implementálja a await
kifejezést. Ha az aktuális szálat úgy blokkolja, hogy szinkron módon várjon egy Task
elem befejezésére, az holtpontokhoz és blokkolt környezeti szálakhoz vezethet. Ez a programozási megközelítés összetettebb hibakezelést igényelhet. Az alábbi táblázat útmutatást nyújt arra, hogyan lehet a feladatok eredményeihez nem blokkoló módon hozzáférni.
Tevékenységforgatókönyv | Aktuális kód | Cserélje le a "await" (várakozás) helyére |
---|---|---|
Háttérfeladat eredményének lekérése |
Task.Wait vagy Task.Result |
await |
Folytatás, ha bármely tevékenység befejeződik | Task.WaitAny |
await Task.WhenAny |
Folytatás, ha minden tevékenység befejeződik | Task.WaitAll |
await Task.WhenAll |
Folytatás bizonyos idő után | Thread.Sleep |
await Task.Delay |
Fontolja meg a ValueTask típus használatát
Ha egy aszinkron metódus egy Task
objektumot ad vissza, előfordulhat, hogy bizonyos útvonalak teljesítménybeli szűk keresztmetszeteket vezetnek be. Mivel Task
hivatkozástípus, a rendszer egy Task
objektumot foglal le a halomból. Ha a async
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égeket okozhatnak a kód kritikus fontosságú szakaszaiban. Ez a forgatókönyv költségessé válhat, ha a foglalások szoros ciklusokban történnek. További információ: általános aszinkron visszatérési típusok.
Mikor érdemes beállítani a ConfigureAwait(false) értéket?
A fejlesztők gyakran érdeklődnek arról, hogy mikor érdemes használni a Task.ConfigureAwait(Boolean) logikai értéket. Ez az API lehetővé teszi, hogy egy Task
-példány konfigurálja annak az állapotgépnek a környezetét, amely bármilyen await
kifejezést implementál. Ha a logikai érték nincs megfelelően beállítva, a teljesítmény csökkenhet, vagy holtpontok léphetnek fel. További információ: ConfigureAwait FAQ.
Kevésbé állapotalapú kódot írj
Kerülje a globális objektumok állapotától vagy bizonyos metódusok végrehajtásától függő kód írását. Ehelyett csak a metódusok visszatérési értékeitől függ. A kevésbé állapotfüggő kódnak számos előnye van:
- Könnyebb érteni a kódot
- Kód egyszerűbb tesztelése
- Egyszerűbb az aszinkron és szinkron kód keverése
- Képes elkerülni a versenyfeltételeket a kódban
- Egyszerűen koordinálható aszinkron kód, amely a visszatérési értékektől függ
- (Bónusz) Jól működik a függőséginjektálással a kódban
Ajánlott cél a teljes vagy majdnem teljes hivatkozási átláthatóság elérése a kódban. Ez a megközelítés kiszámítható, tesztelhető és karbantartható kódbázist eredményez.
Tekintse át a teljes példát
Az alábbi kód a teljes példát jelöli, amely a Program.cs példafájlban érhető el.
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.