Megosztás a következőn keresztül:


Aszinkron programozási forgatókönyvek

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 vagy Task<T> objektum jelöl a async 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();

További információt a cikk végén teljes példakódban talál.

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> és Task 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 a await 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 módosítót és a kifejezést használja a metódus nélkül.

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:

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.