Asenkron görevleri tamamlandıkça işleme (C#)

Task.WhenAny kullanarak, birden fazla görevi aynı anda başlatabilir ve başladıkları sıraya göre işlem yapmak yerine, tamamlandıklarında tek tek işleyebilirsiniz.

Aşağıdaki örnek, bir görev koleksiyonu oluşturmak için bir sorgu kullanır. Her görev, belirtilen bir web sitesinin içeriğini indirir. Bir while döngüsünün her yinelemesinde, beklenen WhenAny çağrısı, indirme işlemini ilk tamamlayan görevlerin koleksiyonundaki görevi döndürür. Bu görev koleksiyondan kaldırılır ve işlenir. Döngü, koleksiyonda görev kalmayana kadar tekrarlamaya devam eder.

Önkoşullar

Aşağıdaki seçeneklerden birini kullanarak bu öğreticiyi izleyebilirsiniz:

  • Visual Studio 2022 üzerinde kurulu .NET masaüstü geliştirme iş yükü. Bu iş yükünü seçtiğinizde .NET SDK'sı otomatik olarak yüklenir.
  • Visual Studio Code gibi, seçtiğiniz bir kod düzenleyicisine sahip .NET SDK.

Örnek uygulama oluşturma

Yeni bir .NET Core konsol uygulaması oluşturun. Dotnet yeni konsol komutunu kullanarak veya Visual Studio'dan oluşturabilirsiniz.

kod düzenleyicinizde Program.cs dosyasını açın ve var olan kodu şu kodla değiştirin:

using System.Diagnostics;

namespace ProcessTasksAsTheyFinish;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

Alan ekle

Sınıf tanımına Program aşağıdaki iki alanı ekleyin:

static readonly HttpClient s_client = new HttpClient
{
    MaxResponseContentBufferSize = 1_000_000
};

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/dynamics365",
    "https://learn.microsoft.com/education",
    "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://learn.microsoft.com/system-center",
    "https://learn.microsoft.com/visualstudio",
    "https://learn.microsoft.com/windows",
    "https://learn.microsoft.com/dotnet/maui"
};

, HttpClient HTTP istekleri gönderme ve HTTP yanıtları alma özelliğini kullanıma sunar. , s_urlList uygulamanın işlemeyi planladığı tüm URL'leri barındırır.

Uygulama giriş noktasını güncelleştirme

Konsol uygulamasının ana giriş noktası yöntemidir Main . Mevcut yöntemi aşağıdakilerle değiştirin:

static Task Main() => SumPageSizesAsync();

Güncellenmiş Main yöntemi artık yürütülebilir dosyaya asenkron bir giriş noktası sağlayan Async ana işlevi olarak kabul edilir. çağrısı SumPageSizesAsyncolarak ifade edilir.

Zaman uyumsuz toplam sayfa boyutları yöntemini oluşturma

Main yönteminin altına SumPageSizesAsync yöntemini ekleyin.

static async Task SumPageSizesAsync()
{
    var stopwatch = Stopwatch.StartNew();

    IEnumerable<Task<int>> downloadTasksQuery =
        from url in s_urlList
        select ProcessUrlAsync(url, s_client);

    List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

    int total = 0;
    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

    stopwatch.Stop();

    Console.WriteLine($"\nTotal bytes returned:  {total:#,#}");
    Console.WriteLine($"Elapsed time:          {stopwatch.Elapsed}\n");
}

döngü, while her yinelemedeki görevlerden birini kaldırır. Her görev tamamlandıktan sonra döngü sona erer. Yöntem, bir örnek oluşturulup başlatılarak Stopwatch başlar. Ardından, yürütüldüğünde bir görev koleksiyonu oluşturan bir sorgu içerir. Aşağıdaki kodda, her bir ProcessUrlAsync çağrısı TResult döndürür; burada, Task<TResult> bir tamsayıdır.

IEnumerable<Task<int>> downloadTasksQuery =
    from url in s_urlList
    select ProcessUrlAsync(url, s_client);

LINQ ile ertelenen yürütme nedeniyle, her görevi başlatmak için Enumerable.ToList çağırmanız gerekir.

List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

Döngü, while koleksiyondaki her görev için aşağıdaki adımları gerçekleştirir:

  1. Koleksiyonun indirme işlemini tamamlayan ilk görevini tanımlamak için bir çağrı WhenAny bekler.

    Task<int> finishedTask = await Task.WhenAny(downloadTasks);
    
  2. Bu görevi koleksiyondan kaldırır.

    downloadTasks.Remove(finishedTask);
    
  3. awaits finishedTask, çağrısı ProcessUrlAsynctarafından döndürülür. finishedTask değişkeni, TResult bir tamsayı olan Task<TResult>'dir. Görev zaten tamamlandı, ancak aşağıdaki örnekte gösterildiği gibi indirilen web sitesinin uzunluğunu almayı bekliyorsunuz. Görev hatalıysa, await içinde depolanan ilk alt özel durumu fırlatır; bu, Task<TResult>.Result özelliğini okumaktan farklıdır, çünkü Task<TResult>.Result özelliğini okumak, AggregateException fırlatacaktır.

    total += await finishedTask;
    

İşlem yöntemi ekleme

Aşağıdaki ProcessUrlAsync yöntemini SumPageSizesAsync yönteminin altına ekleyin:

static async Task<int> ProcessUrlAsync(string url, HttpClient client)
{
    byte[] content = await client.GetByteArrayAsync(url);
    Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

    return content.Length;
}

Herhangi bir URL için, yöntem sağlanan client örneğini kullanarak yanıtı byte[] olarak alacaktır. URL ve uzunluk konsola yazıldıktan sonra, uzunluk döndürülür.

İndirilen uzunlukların her zaman aynı sırada görünmediğini doğrulamak için programı birkaç kez çalıştırın.

Dikkat

Az sayıda görev içeren sorunları çözmek için örnekte açıklandığı gibi bir döngüde kullanabilirsiniz WhenAny . Ancak, işlenmek üzere çok sayıda göreviniz varsa diğer yaklaşımlar daha verimlidir. Daha fazla bilgi ve örnek için bkz. Görevler tamamlandığında işleme.

Yaklaşımı Task.WhenEach kullanarak basitleştirin.

while metodunda SumPageSizesAsync uygulanan döngü, .NET 9'da tanıtılan yeni Task.WhenEach metodunu await foreach döngüsünde çağırarak basitleştirilebilir.
Daha önce uygulanan while döngünün yerine:

    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

Basitleştirilmiş await foreach ile

    await foreach (Task<int> t in Task.WhenEach(downloadTasks))
    {
        total += await t;
    }

Bu yeni yaklaşım, bir görevin el ile çağrılıp tamamlananın kaldırılması için tekrar tekrar Task.WhenAny çağrısı yapmayı gerektirmez, çünkü Task.WhenEach görevler tamamlanma sırasına göre yinelenir.

Tam örnek

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

using System.Diagnostics;

HttpClient s_client = new()
{
    MaxResponseContentBufferSize = 1_000_000
};

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/dynamics365",
    "https://learn.microsoft.com/education",
    "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://learn.microsoft.com/system-center",
    "https://learn.microsoft.com/visualstudio",
    "https://learn.microsoft.com/windows",
    "https://learn.microsoft.com/dotnet/maui"
};

await SumPageSizesAsync();

async Task SumPageSizesAsync()
{
    var stopwatch = Stopwatch.StartNew();

    IEnumerable<Task<int>> downloadTasksQuery =
        from url in s_urlList
        select ProcessUrlAsync(url, s_client);

    List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

    int total = 0;
    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

    stopwatch.Stop();

    Console.WriteLine($"\nTotal bytes returned:    {total:#,#}");
    Console.WriteLine($"Elapsed time:              {stopwatch.Elapsed}\n");
}

static async Task<int> ProcessUrlAsync(string url, HttpClient client)
{
    byte[] content = await client.GetByteArrayAsync(url);
    Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

    return content.Length;
}

// Example output:
// https://learn.microsoft.com                                      132,517
// https://learn.microsoft.com/powershell                            57,375
// https://learn.microsoft.com/gaming                                33,549
// https://learn.microsoft.com/aspnet/core                           88,714
// https://learn.microsoft.com/surface                               39,840
// https://learn.microsoft.com/enterprise-mobility-security          30,903
// https://learn.microsoft.com/microsoft-365                         67,867
// https://learn.microsoft.com/windows                               26,816
// https://learn.microsoft.com/dotnet/maui                           57,958
// https://learn.microsoft.com/dotnet                                78,706
// https://learn.microsoft.com/graph                                 48,277
// https://learn.microsoft.com/dynamics365                           49,042
// https://learn.microsoft.com/office                                67,867
// https://learn.microsoft.com/system-center                         42,887
// https://learn.microsoft.com/education                             38,636
// https://learn.microsoft.com/azure                                421,663
// https://learn.microsoft.com/visualstudio                          30,925
// https://learn.microsoft.com/sql                                   54,608
// https://learn.microsoft.com/azure/devops                          86,034

// Total bytes returned:    1,454,184
// Elapsed time:            00:00:01.1290403

Ayrıca bakınız