📌 ⚡ C# / .NET
C# async/await puska: Task, CancellationToken és gyakori hibák
Az
> **A legfontosabb szabály:** az aszinkron programozás nem feltétlenül teszi gyorsabbá magát a műveletet. Elsősorban azt teszi lehetővé, hogy a program a várakozás ideje alatt más munkát is végezhessen.
## Hogyan működik az
Az
- A
- Az
- Egy
- Ilyenkor a vezérlés visszatér a hívóhoz, ezért a szál más munkát végezhet.
- Amikor a várt művelet befejeződik, a metódus végrehajtása folytatódik.
- A folytatás nem feltétlenül ugyanazon a szálon történik.
Az
> „Várd meg ezt a műveletet anélkül, hogy közben blokkolnád a jelenlegi szálat.”
### Egyszerű példa
A
## Az aszinkron metódusok visszatérési típusai
| Visszatérési típus | Használat |
|---|---|
|
|
|
|
###
Akkor használjuk, ha a művelet nem ad vissza eredményt:
###
Akkor használjuk, ha a művelet eredményt is visszaad:
A hívó oldalon az
###
A
Nem általános helyettesítője a
## Az
Az aszinkron metódusok nevét általában
Ez nem fordítói követelmény, hanem .NET-konvenció. A névből így azonnal látható, hogy a metódus jellemzően
Eseménykezelőknél általában nem szükséges az
## I/O-korlátos és CPU-korlátos műveletek
Az aszinkron programozás helyes használatához meg kell különböztetni az I/O-korlátos és a CPU-korlátos feladatokat.
### I/O-korlátos műveletek
Ilyenek például:
- HTTP-kérések;
- adatbázis-lekérdezések;
- fájlműveletek;
- hálózati kommunikáció;
- külső szolgáltatások válaszának megvárása.
Ezekhez a rendelkezésre álló natív aszinkron API-t használjuk:
Nem szükséges
### CPU-korlátos műveletek
Ilyenek például:
- képfeldolgozás;
- tömörítés;
- nagy mennyiségű adat számítása;
- titkosítás;
- összetett matematikai műveletek.
Asztali vagy mobilalkalmazásban a
A számítást végző kódnak magának is figyelnie kell a tokent. A
ASP.NET Core-ban nem érdemes rutinszerűen
## Szekvenciális vagy egyidejű végrehajtás?
Az egymás után elhelyezett
Ez helyes, ha:
- a második művelet függ az első eredményétől;
- fontos a végrehajtási sorrend;
- nem akarjuk egyszerre terhelni a külső szolgáltatást;
- a műveletek nem futtathatók biztonságosan egyidejűleg.
Ha a műveletek egymástól függetlenek, előbb elindíthatjuk őket, majd
A
A
### Ne indíts korlátlan számú műveletet
A következő megoldás kis elemszámnál megfelelő lehet:
Több ezer elem esetén azonban ez túl sok hálózati kapcsolatot, adatbázis-lekérdezést vagy más erőforrás-igényes műveletet indíthat egyszerre.
Ilyenkor korlátozni kell az egyidejű végrehajtások számát, például
##
A
A tipikus szerepkörök:
- a
- a
- a végrehajtott művelet figyeli a tokent;
- megszakítás esetén általában
A token paraméterét konvenció szerint a paraméterlista végére helyezzük:
A tokent a teljes hívási láncon tovább kell adni:
Hosszabb számítási ciklusokban a kódnak rendszeresen ellenőriznie kell a megszakítást:
### Időkorlát és külső megszakítás összekapcsolása
A művelet így megszakítási kérést kap, ha a hívó megszakítja, vagy letelik a tíz másodperces időkorlát.
## Kivételkezelés aszinkron kódban
A
Ezért a
Csak olyan kivételt érdemes elkapni, amelyet az adott kódrészlet valóban kezelni tud. A kivétel naplózása, majd teljes figyelmen kívül hagyása gyakran hibás állapotot rejt el.
### Több hiba
Ha több feladat is hibával fejeződik be, az összes kivétel megtalálható a
A
## Miért kerülendő az
A következő metódus hibásan van megtervezve:
Az ilyen metódus:
- nem várható meg
- nehezen illeszthető más aszinkron műveletekhez;
- nehezen tesztelhető;
- a hívó nem tudja biztosan, mikor fejeződött be;
- a kivételei nem a megszokott
A helyes visszatérési típus:
Az
Mivel az eseménykezelőt a hívó nem tudja
## Kerüld a
Az aszinkron műveletek szinkron blokkolása gyakori hibaforrás:
A helyes megoldás:
A
- blokkolja a hívó szálat;
- felhasználói felületeknél lefagyást okozhat;
- klasszikus ASP.NET-ben és UI-környezetekben holtpontot okozhat;
- szerveralkalmazásokban ThreadPool-kimerüléshez vezethet;
- csökkentheti az alkalmazás áteresztőképességét.
A következő megoldás sem általános javítás:
Ez továbbra is szinkron módon blokkol. A legjobb megközelítés az aszinkron működés végigvezetése a teljes hívási láncon: **async all the way**.
## A „fire-and-forget” nem lesz biztonságos attól, hogy eldobod a
A következő hívás nincs megvárva:
A fordítói figyelmeztetés elnémítható:
Ez azonban nem teszi biztonságossá a műveletet. Továbbra is fennállhat, hogy:
- a kivétel nem kerül megfelelően kezelésre;
- a folyamat vagy a kérés befejeződik a művelet előtt;
- egy ASP.NET Core-kéréshez tartozó scoped szolgáltatás már felszabadult;
- az alkalmazás leállásakor a munka elveszik;
- nincs retry, naplózás vagy állapotkövetés.
Megbízható háttérmunkához használjunk háttérfeladat-sort,
## Mikor felesleges az
Ha egy metódus csak változtatás nélkül visszaad egy másik
Feleslegesen bőbeszédű változat:
Egyszerűbb változat:
Az egyszerűsítés nem mindig alkalmazható. Az
-
-
- az eredményt még fel kell dolgozni;
- több aszinkron lépést kell összefűzni;
- a kivétel létrejöttének és megfigyelésének időzítése számít.
##
A
Gyakorlati irányelvek:
- ne használd mechanikusan minden
- UI-kódban gyakran szükség van az eredeti környezetre;
- újrahasználható, alkalmazásfüggetlen könyvtárakban gyakran indokolt lehet;
- ASP.NET Core-ban alapértelmezés szerint nincs a klasszikus ASP.NET-hez vagy UI-keretrendszerekhez hasonló
A használatáról mindig az adott alkalmazástípus és a folytatás környezeti igénye alapján döntsünk.
## Összetett példa
A következő példa két egymástól független HTTP-kérést indít el, közös megszakítási tokent használ, és tíz másodperces időkorlátot állít be:
A példában:
1. a külső megszakítás és a tíz másodperces időkorlát össze van kapcsolva;
2. a két HTTP-kérés egymástól függetlenül indul el;
3. a
4. a két típusos
5. a
6. egyik HTTP-kérést sem csomagoljuk feleslegesen
## Gyakori hibák röviden
| Kerülendő | Javasolt |
|---|---|
|
|
| I/O-művelet
| Független műveletek soros megvárása |
| A token továbbadásának kihagyása | Token a teljes hívási láncon |
| Korlátlan számú egyidejű feladat | Korlátozott konkurencia |
| A
| Minden kivétel elnyelése | Célzott kezelés vagy továbbdobás |
|
## Gyors ellenőrzőlista
Aszinkron metódus írásakor ellenőrizd az alábbiakat:
- A metódus
- A neve általában
- Nem használ
- A
- A tokent minden támogatott alsóbb szintű művelethez továbbadja.
- Az egymástól független I/O-műveleteket szükség esetén egyidejűleg indítja el.
- Az egyidejű műveletek számát korlátozza, ha a bemeneti lista nagy lehet.
- Az I/O-műveleteket nem csomagolja feleslegesen
- Az
- Minden elindított
- A kivételeket azon a szinten kezeli, ahol valóban lehet velük valamit kezdeni.
- A megszakítást a vezérlési folyamat lehetséges eredményeként kezeli.
## Összefoglalás
Az
A legfontosabb irányelvek:
- I/O-műveleteknél használd a natív aszinkron API-kat.
- Ne blokkolj aszinkron műveletet
- Az
- A megszakítási tokent add tovább a teljes hívási láncon.
- A független feladatokat szükség esetén
- Ne indíts korlátlan számú egyidejű műveletet.
- A hosszú háttérmunkát ne „fire-and-forget” hívással, hanem erre kialakított feldolgozóval kezeld.
> **Az aszinkron végrehajtást a teljes hívási láncon keresztül kell vezetni.**
## Források és további olvasnivaló
- [Asynchronous programming with async and await — Microsoft Learn](https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/)
- [await operator — Microsoft Learn](https://learn.microsoft.com/dotnet/csharp/language-reference/operators/await)
- [Async return types — Microsoft Learn](https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/async-return-types)
- [CancellationToken — Microsoft Learn](https://learn.microsoft.com/dotnet/api/system.threading.cancellationtoken)
- [Task.WhenAll — Microsoft Learn](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.whenall)
- [ASP.NET Core performance best practices — Microsoft Learn](https://learn.microsoft.com/aspnet/core/fundamentals/best-practices)
- [ConfigureAwait FAQ — .NET Blog](https://devblogs.microsoft.com/dotnet/configureawait-faq/)
async és az await a modern C# alapvető eszközei. Segítségükkel úgy várhatunk meg hosszabb ideig tartó műveleteket — például HTTP-kéréseket, adatbázis-lekérdezéseket vagy fájlműveleteket —, hogy közben ne blokkoljuk feleslegesen a hívó szálat.> **A legfontosabb szabály:** az aszinkron programozás nem feltétlenül teszi gyorsabbá magát a műveletet. Elsősorban azt teszi lehetővé, hogy a program a várakozás ideje alatt más munkát is végezhessen.
## Hogyan működik az
async és az await?Az
async önmagában nem indít új szálat, a Task pedig nem azonos egy háttérszállal.- A
Task egy folyamatban lévő vagy később befejeződő műveletet reprezentál.- Az
async módosító lehetővé teszi az await használatát a metódusban.- Egy
async metódus szinkron módon fut az első olyan await pontig, ahol a várt művelet még nem fejeződött be.- Ilyenkor a vezérlés visszatér a hívóhoz, ezért a szál más munkát végezhet.
- Amikor a várt művelet befejeződik, a metódus végrehajtása folytatódik.
- A folytatás nem feltétlenül ugyanazon a szálon történik.
Az
await tehát nem azt jelenti, hogy „indíts új szálat”, hanem azt, hogy:> „Várd meg ezt a műveletet anélkül, hogy közben blokkolnád a jelenlegi szálat.”
### Egyszerű példa
public static async Task<string> DownloadPageAsync(
HttpClient httpClient,
string url,
CancellationToken cancellationToken = default)
{
using HttpResponseMessage response =
await httpClient.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken);
}A
GetAsync hívás közben a programnak nem kell egy szálat kizárólag arra használnia, hogy az a hálózati válaszra várjon.## Az aszinkron metódusok visszatérési típusai
| Visszatérési típus | Használat |
|---|---|
|
Task | A műveletnek nincs visszatérési értéke, de meg kell várni a befejeződését ||
Task<T> | A művelet egy T típusú eredményt ad vissza ||
void | Szinte kizárólag eseménykezelőknél ||
ValueTask<T> | Speciális, teljesítménykritikus esetekben, mérés alapján |###
TaskAkkor használjuk, ha a művelet nem ad vissza eredményt:
public async Task SaveSettingsAsync(
Settings settings,
CancellationToken cancellationToken = default)
{
await _repository.SaveAsync(settings, cancellationToken);
}###
Task<T>Akkor használjuk, ha a művelet eredményt is visszaad:
public async Task<User?> GetUserAsync(
int userId,
CancellationToken cancellationToken = default)
{
return await _repository.GetUserAsync(userId, cancellationToken);
}A hívó oldalon az
await eredménye már közvetlenül User? típusú:User? user = await GetUserAsync(userId, cancellationToken);###
ValueTask<T>A
ValueTask<T> bizonyos esetekben csökkentheti az objektumfoglalások számát, például akkor, ha egy gyakran hívott metódus nagyon sokszor szinkron módon, gyorsítótárból ad vissza eredményt.Nem általános helyettesítője a
Task<T> típusnak. Összetettebb használati szabályai miatt alapértelmezésként továbbra is a Task és a Task<T> javasolt.## Az
Async utótag használataAz aszinkron metódusok nevét általában
Async utótaggal látjuk el:GetUserAsync()
SaveOrderAsync()
SendEmailAsync()
LoadConfigurationAsync()Ez nem fordítói követelmény, hanem .NET-konvenció. A névből így azonnal látható, hogy a metódus jellemzően
Task vagy Task<T> értéket ad vissza, és await használatával kell meghívni.Eseménykezelőknél általában nem szükséges az
Async utótag:private async void SaveButton_Click(object? sender, EventArgs e)
{
await SaveAsync();
}## I/O-korlátos és CPU-korlátos műveletek
Az aszinkron programozás helyes használatához meg kell különböztetni az I/O-korlátos és a CPU-korlátos feladatokat.
### I/O-korlátos műveletek
Ilyenek például:
- HTTP-kérések;
- adatbázis-lekérdezések;
- fájlműveletek;
- hálózati kommunikáció;
- külső szolgáltatások válaszának megvárása.
Ezekhez a rendelkezésre álló natív aszinkron API-t használjuk:
string content =
await httpClient.GetStringAsync(url, cancellationToken);Nem szükséges
Task.Run blokkba csomagolni:// Kerülendő: felesleges ThreadPool-ütemezés
string content = await Task.Run(
() => httpClient.GetStringAsync(url, cancellationToken),
cancellationToken);### CPU-korlátos műveletek
Ilyenek például:
- képfeldolgozás;
- tömörítés;
- nagy mennyiségű adat számítása;
- titkosítás;
- összetett matematikai műveletek.
Asztali vagy mobilalkalmazásban a
Task.Run segíthet abban, hogy a hosszú számítás ne blokkolja a felhasználói felület szálát:CalculationResult result = await Task.Run(
() => CalculateResult(input, cancellationToken),
cancellationToken);A számítást végző kódnak magának is figyelnie kell a tokent. A
Task.Run számára átadott token önmagában nem állít le egy már futó számítást.ASP.NET Core-ban nem érdemes rutinszerűen
Task.Run mögé rejteni a szinkron vagy CPU-igényes munkát. A hosszú, erőforrás-igényes feladatokat célszerű háttérfolyamatba vagy külön munkafeldolgozó szolgáltatásba helyezni.## Szekvenciális vagy egyidejű végrehajtás?
Az egymás után elhelyezett
await hívások szekvenciálisan hajtják végre a műveleteket:User user = await GetUserAsync(userId, cancellationToken);
IReadOnlyList<Order> orders =
await GetOrdersAsync(userId, cancellationToken);Ez helyes, ha:
- a második művelet függ az első eredményétől;
- fontos a végrehajtási sorrend;
- nem akarjuk egyszerre terhelni a külső szolgáltatást;
- a műveletek nem futtathatók biztonságosan egyidejűleg.
Ha a műveletek egymástól függetlenek, előbb elindíthatjuk őket, majd
Task.WhenAll segítségével megvárhatjuk a befejeződésüket:Task<User> userTask =
GetUserAsync(userId, cancellationToken);
Task<IReadOnlyList<Order>> ordersTask =
GetOrdersAsync(userId, cancellationToken);
await Task.WhenAll(userTask, ordersTask);
User user = await userTask;
IReadOnlyList<Order> orders = await ordersTask;A
Task.WhenAll után végrehajtott újabb await nem indítja el ismét a műveleteket. A feladatok ekkor már befejeződtek; az await csak a típusos eredményüket olvassa ki.A
Task.WhenAll nem feltétlenül jelent párhuzamos végrehajtást több processzormagon. I/O-műveleteknél azt jelenti, hogy több várakozási idő átfedheti egymást.### Ne indíts korlátlan számú műveletet
A következő megoldás kis elemszámnál megfelelő lehet:
Task<Product>[] tasks = productIds
.Select(id => LoadProductAsync(id, cancellationToken))
.ToArray();
Product[] products = await Task.WhenAll(tasks);Több ezer elem esetén azonban ez túl sok hálózati kapcsolatot, adatbázis-lekérdezést vagy más erőforrás-igényes műveletet indíthat egyszerre.
Ilyenkor korlátozni kell az egyidejű végrehajtások számát, például
SemaphoreSlim, Parallel.ForEachAsync, Channel-alapú feldolgozás vagy háttérfeladat-sor használatával.##
CancellationToken: együttműködésen alapuló megszakításA
CancellationToken nem állít le erőszakkal egy műveletet. A hívó jelzi, hogy már nincs szüksége az eredményre, a végrehajtott művelet pedig együttműködik a megszakításban.A tipikus szerepkörök:
- a
CancellationTokenSource kezdeményezi a megszakítást;- a
CancellationToken továbbítja a megszakítási kérést;- a végrehajtott művelet figyeli a tokent;
- megszakítás esetén általában
OperationCanceledException keletkezik.A token paraméterét konvenció szerint a paraméterlista végére helyezzük:
public async Task<string> LoadReportAsync(
int reportId,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
using HttpResponseMessage response = await _httpClient.GetAsync(
$"api/reports/{reportId}",
cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken);
}A tokent a teljes hívási láncon tovább kell adni:
public async Task<Report> CreateReportAsync(
int reportId,
CancellationToken cancellationToken = default)
{
string content =
await LoadReportAsync(reportId, cancellationToken);
return await _reportParser.ParseAsync(
content,
cancellationToken);
}Hosszabb számítási ciklusokban a kódnak rendszeresen ellenőriznie kell a megszakítást:
public static long CalculateTotal(
IEnumerable<int> values,
CancellationToken cancellationToken)
{
long total = 0;
foreach (int value in values)
{
cancellationToken.ThrowIfCancellationRequested();
total += ExpensiveCalculation(value);
}
return total;
}### Időkorlát és külső megszakítás összekapcsolása
public async Task<string> LoadWithTimeoutAsync(
int reportId,
CancellationToken cancellationToken = default)
{
using var timeoutCts =
new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var linkedCts =
CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
timeoutCts.Token);
return await LoadReportAsync(reportId, linkedCts.Token);
}A művelet így megszakítási kérést kap, ha a hívó megszakítja, vagy letelik a tíz másodperces időkorlát.
## Kivételkezelés aszinkron kódban
A
Task vagy Task<T> típusú aszinkron metódus kivétele a visszaadott feladatban tárolódik, majd az await pontján újra megjelenik.Ezért a
try-catch blokkot az await köré helyezzük:try
{
string content =
await _httpClient.GetStringAsync(url, cancellationToken);
ProcessContent(content);
}
catch (OperationCanceledException)
when (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("A műveletet a hívó megszakította.");
}
catch (HttpRequestException exception)
{
Console.WriteLine(
$"A HTTP-kérés sikertelen: {exception.Message}");
}Csak olyan kivételt érdemes elkapni, amelyet az adott kódrészlet valóban kezelni tud. A kivétel naplózása, majd teljes figyelmen kívül hagyása gyakran hibás állapotot rejt el.
### Több hiba
Task.WhenAll eseténHa több feladat is hibával fejeződik be, az összes kivétel megtalálható a
WhenAll által visszaadott feladat Exception.InnerExceptions gyűjteményében:Task combinedTask = Task.WhenAll(tasks);
try
{
await combinedTask;
}
catch
{
if (combinedTask.Exception is not null)
{
foreach (Exception exception
in combinedTask.Exception.InnerExceptions)
{
Console.WriteLine(exception.Message);
}
}
throw;
}A
throw; megőrzi az eredeti kivételinformációt.## Miért kerülendő az
async void?A következő metódus hibásan van megtervezve:
public async void SaveOrderAsync(Order order)
{
await _repository.SaveAsync(order);
}Az ilyen metódus:
- nem várható meg
await használatával;- nehezen illeszthető más aszinkron műveletekhez;
- nehezen tesztelhető;
- a hívó nem tudja biztosan, mikor fejeződött be;
- a kivételei nem a megszokott
Task-alapú módon jutnak vissza a hívóhoz.A helyes visszatérési típus:
public async Task SaveOrderAsync(
Order order,
CancellationToken cancellationToken = default)
{
await _repository.SaveAsync(order, cancellationToken);
}Az
async void legfontosabb elfogadható használati esete az eseménykezelő:private async void SaveButton_Click(object? sender, EventArgs e)
{
try
{
await SaveOrderAsync(_currentOrder);
}
catch (Exception exception)
{
ShowError(exception.Message);
}
}Mivel az eseménykezelőt a hívó nem tudja
await segítségével megvárni, a kivételeket célszerű magában az eseménykezelőben kezelni.## Kerüld a
.Result és .Wait() használatátAz aszinkron műveletek szinkron blokkolása gyakori hibaforrás:
// Kerülendő
User user = GetUserAsync(userId).Result;
// Kerülendő
GetUserAsync(userId).Wait();A helyes megoldás:
User user =
await GetUserAsync(userId, cancellationToken);A
.Result és a .Wait():- blokkolja a hívó szálat;
- felhasználói felületeknél lefagyást okozhat;
- klasszikus ASP.NET-ben és UI-környezetekben holtpontot okozhat;
- szerveralkalmazásokban ThreadPool-kimerüléshez vezethet;
- csökkentheti az alkalmazás áteresztőképességét.
A következő megoldás sem általános javítás:
User user = GetUserAsync(userId)
.GetAwaiter()
.GetResult();Ez továbbra is szinkron módon blokkol. A legjobb megközelítés az aszinkron működés végigvezetése a teljes hívási láncon: **async all the way**.
## A „fire-and-forget” nem lesz biztonságos attól, hogy eldobod a
Task értékétA következő hívás nincs megvárva:
SendNotificationAsync();A fordítói figyelmeztetés elnémítható:
_ = SendNotificationAsync();Ez azonban nem teszi biztonságossá a műveletet. Továbbra is fennállhat, hogy:
- a kivétel nem kerül megfelelően kezelésre;
- a folyamat vagy a kérés befejeződik a művelet előtt;
- egy ASP.NET Core-kéréshez tartozó scoped szolgáltatás már felszabadult;
- az alkalmazás leállásakor a munka elveszik;
- nincs retry, naplózás vagy állapotkövetés.
Megbízható háttérmunkához használjunk háttérfeladat-sort,
BackgroundService implementációt, Channel-alapú feldolgozót, Hangfire-t vagy külső üzenetsort.## Mikor felesleges az
async és az await?Ha egy metódus csak változtatás nélkül visszaad egy másik
Task értéket, gyakran nincs szükség az async és az await használatára.Feleslegesen bőbeszédű változat:
public async Task<User?> GetUserAsync(
int userId,
CancellationToken cancellationToken = default)
{
return await _repository.GetUserAsync(
userId,
cancellationToken);
}Egyszerűbb változat:
public Task<User?> GetUserAsync(
int userId,
CancellationToken cancellationToken = default)
{
return _repository.GetUserAsync(
userId,
cancellationToken);
}Az egyszerűsítés nem mindig alkalmazható. Az
async és az await továbbra is szükséges lehet például akkor, ha:-
try-catch vagy finally blokkban akarjuk kezelni az aszinkron műveletet;-
using vagy await using élettartamot kell fenntartani az await végéig;- az eredményt még fel kell dolgozni;
- több aszinkron lépést kell összefűzni;
- a kivétel létrejöttének és megfigyelésének időzítése számít.
##
ConfigureAwait(false) rövidenA
ConfigureAwait(false) azt jelzi, hogy a folytatásnak nem szükséges visszatérnie az eredeti szinkronizációs környezetbe:string content = await LoadContentAsync()
.ConfigureAwait(false);Gyakorlati irányelvek:
- ne használd mechanikusan minden
await után;- UI-kódban gyakran szükség van az eredeti környezetre;
- újrahasználható, alkalmazásfüggetlen könyvtárakban gyakran indokolt lehet;
- ASP.NET Core-ban alapértelmezés szerint nincs a klasszikus ASP.NET-hez vagy UI-keretrendszerekhez hasonló
SynchronizationContext, ezért ott sok esetben nem változtatja meg a működést.A használatáról mindig az adott alkalmazástípus és a folytatás környezeti igénye alapján döntsünk.
## Összetett példa
A következő példa két egymástól független HTTP-kérést indít el, közös megszakítási tokent használ, és tíz másodperces időkorlátot állít be:
using System.Net.Http.Json;
public sealed record UserDto(int Id, string Name);
public sealed record OrderDto(int Id, decimal Total);
public sealed record DashboardDto(
UserDto User,
IReadOnlyList<OrderDto> Orders);
public sealed class DashboardClient
{
private readonly HttpClient _httpClient;
public DashboardClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<DashboardDto> GetDashboardAsync(
int userId,
CancellationToken cancellationToken = default)
{
using var timeoutCts =
new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var linkedCts =
CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
timeoutCts.Token);
CancellationToken token = linkedCts.Token;
Task<UserDto?> userTask =
_httpClient.GetFromJsonAsync<UserDto>(
$"api/users/{userId}",
token);
Task<OrderDto[]?> ordersTask =
_httpClient.GetFromJsonAsync<OrderDto[]>(
$"api/users/{userId}/orders",
token);
await Task.WhenAll(userTask, ordersTask);
UserDto user = await userTask
?? throw new InvalidOperationException(
"A felhasználó nem található.");
OrderDto[] orders =
await ordersTask ?? Array.Empty<OrderDto>();
return new DashboardDto(user, orders);
}
}A példában:
1. a külső megszakítás és a tíz másodperces időkorlát össze van kapcsolva;
2. a két HTTP-kérés egymástól függetlenül indul el;
3. a
Task.WhenAll mindkettőt megvárja;4. a két típusos
await kiolvassa a már befejezett feladatok eredményét;5. a
CancellationToken mindkét alsóbb szintű művelethez eljut;6. egyik HTTP-kérést sem csomagoljuk feleslegesen
Task.Run blokkba.## Gyakori hibák röviden
| Kerülendő | Javasolt |
|---|---|
|
async void általános metódusban | Task vagy Task<T> ||
.Result vagy .Wait() | await || I/O-művelet
Task.Run blokkban | Natív aszinkron API || Független műveletek soros megvárása |
Task.WhenAll || A token továbbadásának kihagyása | Token a teljes hívási láncon |
| Korlátlan számú egyidejű feladat | Korlátozott konkurencia |
| A
Task eredményének eldobása | await vagy háttérfeldolgozó || Minden kivétel elnyelése | Célzott kezelés vagy továbbdobás |
|
ConfigureAwait(false) mechanikusan | Környezetfüggő döntés |## Gyors ellenőrzőlista
Aszinkron metódus írásakor ellenőrizd az alábbiakat:
- A metódus
Task vagy Task<T> típust ad vissza.- A neve általában
Async utótaggal végződik.- Nem használ
.Result, .Wait() vagy más blokkoló várakozást.- A
CancellationToken a paraméterlista végén található.- A tokent minden támogatott alsóbb szintű művelethez továbbadja.
- Az egymástól független I/O-műveleteket szükség esetén egyidejűleg indítja el.
- Az egyidejű műveletek számát korlátozza, ha a bemeneti lista nagy lehet.
- Az I/O-műveleteket nem csomagolja feleslegesen
Task.Run blokkba.- Az
async void kizárólag indokolt eseménykezelőben szerepel.- Minden elindított
Task meg van várva, vagy megbízható háttérfeldolgozóhoz kerül.- A kivételeket azon a szinten kezeli, ahol valóban lehet velük valamit kezdeni.
- A megszakítást a vezérlési folyamat lehetséges eredményeként kezeli.
## Összefoglalás
Az
async és az await a reszponzív és jól skálázható .NET-alkalmazások alapvető eszköze.A legfontosabb irányelvek:
- I/O-műveleteknél használd a natív aszinkron API-kat.
- Ne blokkolj aszinkron műveletet
.Result vagy .Wait() használatával.- Az
async void maradjon az eseménykezelők eszköze.- A megszakítási tokent add tovább a teljes hívási láncon.
- A független feladatokat szükség esetén
Task.WhenAll segítségével várd meg.- Ne indíts korlátlan számú egyidejű műveletet.
- A hosszú háttérmunkát ne „fire-and-forget” hívással, hanem erre kialakított feldolgozóval kezeld.
> **Az aszinkron végrehajtást a teljes hívási láncon keresztül kell vezetni.**
## Források és további olvasnivaló
- [Asynchronous programming with async and await — Microsoft Learn](https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/)
- [await operator — Microsoft Learn](https://learn.microsoft.com/dotnet/csharp/language-reference/operators/await)
- [Async return types — Microsoft Learn](https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/async-return-types)
- [CancellationToken — Microsoft Learn](https://learn.microsoft.com/dotnet/api/system.threading.cancellationtoken)
- [Task.WhenAll — Microsoft Learn](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.whenall)
- [ASP.NET Core performance best practices — Microsoft Learn](https://learn.microsoft.com/aspnet/core/fundamentals/best-practices)
- [ConfigureAwait FAQ — .NET Blog](https://devblogs.microsoft.com/dotnet/configureawait-faq/)