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 vagy Task<T> azon async 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 asyncawaitmegkö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 a await 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:

  1. 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.

  2. 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 awaitané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 ívik le a munkát egy másik szálat.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.

  • asyncmetódusok kell egyawaitkulcsszót a testükben, vagy soha nem hozamot!

    Ezt fontos szem előtt tartani. Ha await nem egy metódus törzsében async 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 voidcsak 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ési Task típusokkal (így nem használhatók és Task<T>nem használhatók). Az egyéb használat async 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.
  • 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 vagy Task.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álatotValueTask

    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 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é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álatotConfigureAwait(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ó: ConfigureAwaitConfigureAwait 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.

Egyéb erőforrások