Zaman uyumsuz programlama senaryoları

G/Ç'ye bağlı gereksinimleriniz varsa (ağdan veri isteme, veritabanına erişme veya dosya sistemine okuma ve yazma gibi), zaman uyumsuz programlamayı kullanmak istersiniz. Zaman uyumsuz kod yazmak için de iyi bir senaryo olan pahalı bir hesaplama gerçekleştirme gibi CPU'ya bağlı kodunuz da olabilir.

C# dil düzeyinde zaman uyumsuz programlama modeline sahiptir. Bu model, geri çağırmaları hokkabazlık yapmak veya zaman uyumsuzluk desteği olan bir kitaplığa uymak zorunda kalmadan zaman uyumsuz kodu kolayca yazmanızı sağlar. Görev Tabanlı Zaman Uyumsuz Desen (TAP) olarak bilinen şeyi izler.

Zaman uyumsuz modele genel bakış

Zaman uyumsuz programlamanın temeli, zaman uyumsuz işlemleri modelleyen ve Task<T> nesneleridirTask. ve anahtar sözcükleri tarafından asyncawait desteklenir. Model çoğu durumda oldukça basittir:

  • G/Ç ile ilişkili kod için, bir yöntemin içinde veya Task<T> döndüren Task bir async işlem beklersiniz.
  • CPU'ya bağlı kod için yöntemiyle Task.Run bir arka plan iş parçacığında başlatılan bir işlemi beklersiniz.

Anahtar await sözcük, sihrin gerçekleştiği yerdir. tarafından gerçekleştirilen awaityöntemi çağırana denetim verir ve sonuçta bir kullanıcı arabiriminin yanıt vermesine veya hizmetin esnek olmasına izin verir. ve awaitdışında async zaman uyumsuz koda yaklaşmanın yolları olsa da, bu makale dil düzeyindeki yapılara odaklanır.

Not

Aşağıdaki örneklerden System.Net.Http.HttpClient bazılarında sınıfı, bir web hizmetinden bazı verileri indirmek için kullanılır. s_httpClient Bu örneklerde kullanılan nesne, sınıfın Program statik bir alanıdır (lütfen tam örneği denetleyin):

private static readonly HttpClient s_httpClient = new();

G/Ç bağlantılı örnek: Web hizmetinden veri indirme

Bir düğmeye basıldığında ancak kullanıcı arabirimi iş parçacığını engellemek istemediğinizde web hizmetinden bazı verileri indirmeniz gerekebilir. Şu şekilde gerçekleştirilebilir:

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);
};

Kod, nesnelerle Task etkileşimde tıkanmadan amacı (verileri zaman uyumsuz olarak indirme) ifade eder.

CPU'ya bağlı örnek: Bir oyun için hesaplama yapma

Bir düğmeye basmanın ekrandaki birçok düşmana zarar verebildiği bir mobil oyun yazdığınızı varsayalım. Hasar hesaplaması yapmak pahalı olabilir ve ui iş parçacığında yapmak, hesaplama yapılırken oyunun duraklatılmış gibi görünmesini sağlar!

Bunu işlemenin en iyi yolu, kullanarak işi Task.Runyapan ve kullanarak awaitsonucunu bekleyen bir arka plan iş parçacığı başlatmaktır. Bu, iş yapılırken kullanıcı arabiriminin rahat hissetmesini sağlar.

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);
};

Bu kod, düğmenin tıklama olayının amacını açıkça ifade eder, arka plan iş parçacığını el ile yönetmeyi gerektirmez ve bunu engellemeyen bir şekilde yapar.

Kapakların altında ne olur?

Öğelerin C# tarafında, derleyici kodunuzu bir öğesine ulaşıldığında await yürütmeyi döndürme ve arka plan işi tamamlandığında yürütmeyi devam ettiren bir durum makinesine dönüştürür.

Teorik olarak eğilimli olan için bu, zaman uyumsuzların Promise Modelinin bir uygulamasıdır.

Anlaşılması gereken önemli parçalar

  • Zaman uyumsuz kod hem G/Ç'ye hem de CPU'ya bağlı kod için kullanılabilir, ancak her senaryo için farklı şekilde kullanılabilir.
  • Zaman uyumsuz kod, Task<T> arka planda yapılan işleri modellemek için kullanılan yapılar olan ve Taskkullanır.
  • anahtar async sözcüğü, bir yöntemi zaman uyumsuz bir yönteme dönüştürür ve bu sayede anahtar sözcüğü gövdesinde kullanabilirsiniz await .
  • Anahtar sözcük uygulandığında await , çağrı yöntemini askıya alır ve beklenen görev tamamlanana kadar denetimi çağırana geri verir.
  • await yalnızca zaman uyumsuz bir yöntem içinde kullanılabilir.

CPU'ya bağlı ve G/Ç bağlı çalışmayı tanıma

Bu kılavuzun ilk iki örneği, G/Ç ve CPU'ya bağlı çalışma için ve'yi await nasıl kullanabileceğinizi async gösterdi. Kodunuzun performansını büyük ölçüde etkileyebileceğinden ve belirli yapıların yanlış kullanımına neden olabileceğinden, yapmanız gereken bir işin G/Ç veya CPU'ya bağlı olduğunu belirleyebilmeniz önemlidir.

Kod yazmadan önce sormanız gereken iki soru şunlardır:

  1. Kodunuz bir veritabanındaki veriler gibi bir şeyi "bekliyor" mu olacak?

    Yanıtınız "evet" ise, çalışmanız G/Ç'ye bağlıdır.

  2. Kodunuz pahalı bir hesaplama gerçekleştirecek mi?

    "Evet" yanıtını verdiyseniz işiniz CPU'ya bağlıdır.

Sahip olduğunuz iş G/Ç'ye bağlıysa ve awaitolmadanTask.Run kullanınasync. Görev Paralel Kitaplığı'nı kullanmamalısınız.

Sahip olduğunuz iş CPU'ya bağlıysa ve yanıt vermeye önem veriyorsanız ve awaitkullanınasync, ancak ile başka bir iş parçacığında Task.Runçalışmayı bırakın. Çalışma eşzamanlılık ve paralellik için uygunsa, Görev Paralel Kitaplığı'nı da kullanmayı göz önünde bulundurun.

Ayrıca, kodunuzun yürütülmesini her zaman ölçmeniz gerekir. Örneğin, çok iş parçacığı kullanıldığında bağlam anahtarlarının yüküne kıyasla CPU'ya bağlı çalışmanızın yeterince maliyetli olmadığı bir durumda kendinizi bulabilirsiniz. Her seçimin bir dezavantajı vardır ve durumunuz için doğru dengeyi seçmelisiniz.

Daha fazla örnek

Aşağıdaki örneklerde C# dilinde zaman uyumsuz kod yazmanın çeşitli yolları gösterilmektedir. Karşılaşabileceğiniz birkaç farklı senaryo ele alınıyor.

Ağdan veri ayıklama

Bu kod parçacığı, verilen URL'den HTML'yi indirir ve HTML'de ".NET" dizesinin kaç kez gerçekleştiğini sayar. Bu görevi gerçekleştiren ve sayıyı döndüren bir Web API denetleyicisi yöntemi tanımlamak için ASP.NET kullanır.

Not

Üretim kodunda HTML ayrıştırma yapmayı planlıyorsanız normal ifadeleri kullanmayın. Bunun yerine ayrıştırma kitaplığı kullanın.

[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;
}

Bir Düğmeye basıldığında aynı görevi gerçekleştiren Evrensel Windows Uygulaması için yazılan senaryonun aynısı aşağıdadır:

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;
}

Birden çok görev tamamlanmasını bekleyin

Kendinizi birden çok veri parçasını eşzamanlı olarak almanız gereken bir durumda bulabilirsiniz. Task API, Task.WhenAllTask.WhenAnybirden çok arka plan işi üzerinde engelleyici olmayan bir bekleme gerçekleştiren zaman uyumsuz kod yazmanıza olanak sağlayan ve olmak üzere iki yöntem içerir.

Bu örnekte, bir dizi sn için verileri nasıl alabileceğiniz User gösterilmektedir 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);
}

LINQ kullanarak bunu daha kısa bir şekilde yazmanın başka bir yolu da aşağıdadır:

private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
    var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
    return await Task.WhenAll(getUserTasks);
}

Daha az kod olsa da LINQ'yi zaman uyumsuz kodla karıştırırken dikkatli olun. LINQ ertelenmiş (gecikmeli) yürütme kullandığından, oluşturulan diziyi veya .ToArray()çağrısıyla yinelemeye zorlamadığınız sürece zaman uyumsuz çağrılar .ToList() döngüde foreach olduğu gibi hemen gerçekleşmez. Yukarıdaki örnek, sorguyu hevesle gerçekleştirmek ve sonuçları bir dizide depolamak için kullanır Enumerable.ToArray . Bu, kodu id => GetUserAsync(id) çalıştırmaya ve görevi başlatmaya zorlar.

Önemli bilgiler ve öneriler

Zaman uyumsuz programlamada beklenmeyen davranışları önleyebilecek bazı ayrıntılar göz önünde bulundurulması gerekir.

  • asyncyöntemlerin vücutlarında birawaitanahtar sözcük olması gerekir, aksi takdirde asla verim vermezler!

    Bu, göz önünde bulundurulması gereken önemli bir konudur. Bir async yöntemin gövdesinde kullanılmazsaawait, C# derleyicisi bir uyarı oluşturur, ancak kod derlenir ve normal bir yöntemmiş gibi çalışır. Zaman uyumsuz yöntem için C# derleyicisi tarafından oluşturulan durum makinesi hiçbir şey başarmadığından, bu inanılmaz derecede verimsizdir.

  • Yazdığınız her zaman uyumsuz yöntem adının soneki olarak "Async" ekleyin.

    Bu, zaman uyumlu ve zaman uyumsuz yöntemleri daha kolay ayırt etmek için .NET'te kullanılan kuraldır. Kodunuz tarafından açıkça çağrılmamış bazı yöntemlerin (olay işleyicileri veya web denetleyicisi yöntemleri gibi) geçerli olması gerekmez. Bunlar kodunuz tarafından açıkça çağrılmadığından, adlandırmaları hakkında açık olmak o kadar önemli değildir.

  • async voidyalnızca olay işleyicileri için kullanılmalıdır.

    async voidolayların dönüş türleri olmadığından zaman uyumsuz olay işleyicilerinin çalışmasına izin vermenin tek yoludur (bu nedenle ve Task<T>'yi Task kullanamaz). Diğer kullanımları async void TAP modelini izlemez ve kullanımı aşağıdakiler gibi zor olabilir:

    • Bir async void yöntemde oluşan özel durumlar bu yöntemin dışında yakalanamaz.
    • async void yöntemleri test etmek zordur.
    • async void yöntemleri, çağıranın zaman uyumsuz olmasını beklemiyorsa hatalı yan etkilere neden olabilir.
  • LINQ ifadelerinde zaman uyumsuz lambda kullanırken dikkatli bir şekilde koşun

    LINQ'teki Lambda ifadeleri ertelenmiş yürütmeyi kullanır; bu da kodun beklediğiniz bir zamanda yürütülebileceği anlamına gelir. Engelleme görevlerinin buna eklenmesi, doğru yazılmaması durumunda kolayca kilitlenmeye neden olabilir. Buna ek olarak, zaman uyumsuz kodun bunun gibi iç içe yerleştirilmiş olması, kodun yürütülmesiyle ilgili mantık yürütmeyi de zorlaştırabilir. Zaman uyumsuz ve LINQ güçlüdür, ancak mümkün olduğunca dikkatli ve net bir şekilde birlikte kullanılmalıdır.

  • Görevleri engelleyici olmayan bir şekilde bekleyen kod yazma

    Geçerli iş parçacığının tamamlanmasını bekleme Task aracı olarak engellenmesi kilitlenmelere ve engellenen bağlam iş parçacıklarına neden olabilir ve daha karmaşık hata işleme gerektirebilir. Aşağıdaki tabloda, görevleri engelleyici olmayan bir şekilde beklemeyle ilgili yönergeler sağlanmaktadır:

    Bunu kullanın... Bunun yerine... Bunu yapmak istediğinizde...
    await Task.Wait veya Task.Result Arka plan görevinin sonucunu alma
    await Task.WhenAny Task.WaitAny Herhangi bir görevin tamamlanmasını bekleme
    await Task.WhenAll Task.WaitAll Tüm görevlerin tamamlanmasını bekleme
    await Task.Delay Thread.Sleep Bir süre bekleniyor
  • Mümkün olduğunca kullanmayıValueTaskgöz önünde bulundurun

    Task Bir nesneyi zaman uyumsuz yöntemlerden döndürmek, belirli yollarda performans sorunlarına neden olabilir. Task bir başvuru türüdür, bu nedenle kullanmak bir nesneyi ayırma anlamına gelir. Değiştirici ile bildirilen bir yöntemin async önbelleğe alınmış bir sonuç döndürdüğü veya zaman uyumlu olarak tamamlandığı durumlarda, ek ayırmalar kodun performans açısından kritik bölümlerinde önemli bir zaman maliyetine dönüşebilir. Bu ayırmalar sıkı döngülerde gerçekleşirse maliyetli hale gelebilir. Daha fazla bilgi için bkz . Genelleştirilmiş zaman uyumsuz dönüş türleri.

  • Kullanmayı göz önünde bulundurunConfigureAwait(false)

    Sık sorulan bir soru şudur: "Yöntemini ne zaman kullanmalıyım Task.ConfigureAwait(Boolean) ?". yöntemi, bir Task örneğin awaiter'ını yapılandırmasına olanak tanır. Bu önemli bir konudur ve yanlış bir şekilde ayarlanması, performans üzerindeki etkileri ve hatta kilitlenmeleri olabilir. hakkında ConfigureAwaitdaha fazla bilgi için bkz . ConfigureAwait SSS.

  • Daha az durum bilgisi olan kod yazma

    Genel nesnelerin durumuna veya belirli yöntemlerin yürütülmesine bağlı değildir. Bunun yerine, yalnızca yöntemlerin dönüş değerlerine bağlıdır. Neden?

    • Kod hakkında daha kolay mantık yürütmesi gerekir.
    • Kodu test etmek daha kolay olacaktır.
    • Zaman uyumsuz ve zaman uyumlu kodu karıştırmak çok daha basittir.
    • Yarış koşulları genellikle tamamen önlenebilir.
    • Dönüş değerlerine bağlı olarak zaman uyumsuz kodun koordinasyonu basitleştirilir.
    • (Bonus) bağımlılık ekleme ile gerçekten iyi çalışır.

Önerilen bir hedef, kodunuzda eksiksiz veya neredeyse eksiksiz Bilgi Saydamlığı elde etmektir. Bunu yaptığınızda tahmin edilebilir, test edilebilir ve sürdürülebilir bir kod tabanı elde edilir.

Tam örnek

Aşağıdaki kod, örneğin Program.cs dosyasının tam metnidir.

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/xamarin"
    };

    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/xamarin: 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.

Diğer kaynaklar