C# async/await puska: Task, CancellationToken és gyakori hibák | Wiki | MagyarFejlesztők.hu
📌 ⚡ C# / .NET

C# async/await puska: Task, CancellationToken és gyakori hibák

✍️ Szerző: @DoomMaker 📅 Frissítve: 2026. 09. 12. 👁️ Megtekintve: 63 alkalommal
Az 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 |

### Task

Akkor 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álata

Az 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ás

A 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én

Ha 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át

Az 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ét

A 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öviden

A 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/)
← Vissza a Wikihez