다음을 통해 공유


비동기 작업 완료 시 처리(C#)

이를 사용하면 Task.WhenAny여러 작업을 동시에 시작하고 작업이 시작된 순서대로 처리하지 않고 완료될 때 하나씩 처리할 수 있습니다.

다음 예제에서는 쿼리를 사용하여 작업 컬렉션을 만듭니다. 각 태스크는 지정된 웹 사이트의 콘텐츠를 다운로드합니다. while 루프의 각 반복에서, 대기된 호출은 태스크 컬렉션 중에서 먼저 다운로드를 완료하는 작업을 반환합니다. 해당 작업은 컬렉션에서 제거되고 처리됩니다. 루프는 컬렉션에 더 이상 태스크가 포함되지 않을 때까지 반복됩니다.

필수 조건

다음 옵션 중 하나를 사용하여 이 자습서를 따를 수 있습니다.

  • Visual Studio 2022에서 .NET 데스크톱 개발 워크로드가 설치됨. 이 워크로드를 선택하면 .NET SDK가 자동으로 설치됩니다.
  • Visual Studio Code와 같이 선택한 코드 편집기가 있는 .NET SDK입니다.

예제 애플리케이션 만들기

새 .NET Core 콘솔 애플리케이션을 만듭니다. dotnet 새 콘솔 명령을 사용하거나 Visual Studio에서 만들 수 있습니다.

코드 편집기에서 Program.cs 파일을 열고 기존 코드를 다음 코드로 바꿉다.

using System.Diagnostics;

namespace ProcessTasksAsTheyFinish;

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

필드 추가

클래스 정의에서 Program 다음 두 필드를 추가합니다.

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

static readonly IEnumerable<string> s_urlList = new string[]
{
    "https://dotnet.territoriali.olinfo.it",
    "https://dotnet.territoriali.olinfo.it/aspnet/core",
    "https://dotnet.territoriali.olinfo.it/azure",
    "https://dotnet.territoriali.olinfo.it/azure/devops",
    "https://dotnet.territoriali.olinfo.it/dotnet",
    "https://dotnet.territoriali.olinfo.it/dynamics365",
    "https://dotnet.territoriali.olinfo.it/education",
    "https://dotnet.territoriali.olinfo.it/enterprise-mobility-security",
    "https://dotnet.territoriali.olinfo.it/gaming",
    "https://dotnet.territoriali.olinfo.it/graph",
    "https://dotnet.territoriali.olinfo.it/microsoft-365",
    "https://dotnet.territoriali.olinfo.it/office",
    "https://dotnet.territoriali.olinfo.it/powershell",
    "https://dotnet.territoriali.olinfo.it/sql",
    "https://dotnet.territoriali.olinfo.it/surface",
    "https://dotnet.territoriali.olinfo.it/system-center",
    "https://dotnet.territoriali.olinfo.it/visualstudio",
    "https://dotnet.territoriali.olinfo.it/windows",
    "https://dotnet.territoriali.olinfo.it/maui"
};

HTTP HttpClient 요청을 보내고 HTTP 응답을 수신하는 기능을 노출합니다. 애플리케이션 s_urlList 에서 처리할 모든 URL을 보유합니다.

애플리케이션 진입점 업데이트

콘솔 애플리케이션의 주요 진입점은 Main 메서드입니다. 기존 메서드를 다음으로 바꿉다.

static Task Main() => SumPageSizesAsync();

이제 업데이트 Main 된 메서드는 실행 파일에 비동기 진입점을 허용하는 비동기 메서드로 간주됩니다. 에 대한 호출로 표현됩니다 SumPageSizesAsync.

비동기 합계 페이지 크기 메서드 만들기

Main 메서드 아래에 메서드를 추가합니다.SumPageSizesAsync

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

루프는 각 반복에서 while 작업 중 하나를 제거합니다. 모든 작업이 완료되면 루프가 종료됩니다. 메서드는 Stopwatch을 인스턴스화하고 시작합니다. 그런 다음, 실행할 때 태스크 컬렉션을 만드는 쿼리를 포함합니다. 다음 코드에서 ProcessUrlAsync에 대한 각 호출은 정수 TResultTask<TResult>을 반환합니다.

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

LINQ를 사용한 지연 실행 으로 인해 각 작업을 시작하도록 호출 Enumerable.ToList 합니다.

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

루프는 while 컬렉션의 각 작업에 대해 다음 단계를 수행합니다.

  1. 컬렉션에서 다운로드를 완료한 첫 번째 작업을 식별하기 위해 WhenAny에 대한 호출을 기다립니다.

    Task<int> finishedTask = await Task.WhenAny(downloadTasks);
    
  2. 컬렉션에서 해당 작업을 제거합니다.

    downloadTasks.Remove(finishedTask);
    
  3. ProcessUrlAsync 호출의 결과를 기다리고, 이는 finishedTask에 의해 반환됩니다. finishedTask 변수는 Task<TResult>이며, TResult는 정수입니다. 작업이 이미 완료되었지만 다음 예제와 같이 다운로드한 웹 사이트의 길이를 검색하기 위해 대기합니다. 태스크에 오류가 있는 경우, await는 속성을 읽을 때 발생하는 AggregateException와 달리 AggregateException에 저장된 첫 번째 자식 예외를 throwTask<TResult>.Result합니다.

    total += await finishedTask;
    

프로세스 메서드 추가

다음 ProcessUrlAsync 메서드를 SumPageSizesAsync 메서드 아래에 추가합니다.

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

지정된 URL의 경우 메서드는 제공된 인스턴스를 client 사용하여 응답을 로 byte[]가져옵니다. URL 및 길이가 콘솔에 기록된 후에 길이가 반환됩니다.

프로그램을 여러 번 실행하여 다운로드한 길이가 항상 같은 순서로 표시되지 않는지 확인합니다.

주의

예제에 설명된 대로 루프에서 사용하면 WhenAny 적은 수의 작업이 포함된 문제를 해결할 수 있습니다. 그러나 처리할 작업이 많은 경우 다른 방법이 더 효율적입니다. 자세한 내용 및 예제는 완료되는 작업 처리를 참조하세요.

를 사용하여 접근 방식 간소화 Task.WhenEach

SumPageSizesAsync 메서드에서 구현된 while 루프는 .NET 9에 도입된 새 await foreach 메서드를 Task.WhenEach 호출하여 await foreach 루프 내에서 간소화할 수 있습니다.
이전에 구현된 while 루프를 바꿉다.

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

간소화된 await foreach을(를) 사용하여 수행합니다.

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

이 새로운 접근 방식을 사용하면 작업을 완료 순서대로 반복 수행하기 때문에 Task.WhenAny를 수동으로 호출하거나 완료되는 작업을 제거하기 위해 더 이상 반복해서 호출할 필요가 없습니다.

전체 예제

다음 코드는 예제에 대한 Program.cs 파일의 전체 텍스트입니다.

using System.Diagnostics;

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

IEnumerable<string> s_urlList = new string[]
{
    "https://dotnet.territoriali.olinfo.it",
    "https://dotnet.territoriali.olinfo.it/aspnet/core",
    "https://dotnet.territoriali.olinfo.it/azure",
    "https://dotnet.territoriali.olinfo.it/azure/devops",
    "https://dotnet.territoriali.olinfo.it/dotnet",
    "https://dotnet.territoriali.olinfo.it/dynamics365",
    "https://dotnet.territoriali.olinfo.it/education",
    "https://dotnet.territoriali.olinfo.it/enterprise-mobility-security",
    "https://dotnet.territoriali.olinfo.it/gaming",
    "https://dotnet.territoriali.olinfo.it/graph",
    "https://dotnet.territoriali.olinfo.it/microsoft-365",
    "https://dotnet.territoriali.olinfo.it/office",
    "https://dotnet.territoriali.olinfo.it/powershell",
    "https://dotnet.territoriali.olinfo.it/sql",
    "https://dotnet.territoriali.olinfo.it/surface",
    "https://dotnet.territoriali.olinfo.it/system-center",
    "https://dotnet.territoriali.olinfo.it/visualstudio",
    "https://dotnet.territoriali.olinfo.it/windows",
    "https://dotnet.territoriali.olinfo.it/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://dotnet.territoriali.olinfo.it                                      132,517
// https://dotnet.territoriali.olinfo.it/powershell                            57,375
// https://dotnet.territoriali.olinfo.it/gaming                                33,549
// https://dotnet.territoriali.olinfo.it/aspnet/core                           88,714
// https://dotnet.territoriali.olinfo.it/surface                               39,840
// https://dotnet.territoriali.olinfo.it/enterprise-mobility-security          30,903
// https://dotnet.territoriali.olinfo.it/microsoft-365                         67,867
// https://dotnet.territoriali.olinfo.it/windows                               26,816
// https://dotnet.territoriali.olinfo.it/maui                               57,958
// https://dotnet.territoriali.olinfo.it/dotnet                                78,706
// https://dotnet.territoriali.olinfo.it/graph                                 48,277
// https://dotnet.territoriali.olinfo.it/dynamics365                           49,042
// https://dotnet.territoriali.olinfo.it/office                                67,867
// https://dotnet.territoriali.olinfo.it/system-center                         42,887
// https://dotnet.territoriali.olinfo.it/education                             38,636
// https://dotnet.territoriali.olinfo.it/azure                                421,663
// https://dotnet.territoriali.olinfo.it/visualstudio                          30,925
// https://dotnet.territoriali.olinfo.it/sql                                   54,608
// https://dotnet.territoriali.olinfo.it/azure/devops                          86,034

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

참고하십시오