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

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

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

필수 조건

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

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

예제 애플리케이션 만들기

새 .NET Core 콘솔 애플리케이션을 만듭니다. dotnet new console 명령 또는 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://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/maui"
};

HttpClient는 HTTP 요청을 보내고 HTTP 응답을 받는 기능을 공개합니다. s_urlList는 애플리케이션이 처리해야 하는 모든 URL을 저장합니다.

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

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

static Task Main() => SumPageSizesAsync();

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

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

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

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를 호출할 때마다 Task<TResult>가 반환됩니다. 여기서 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은 정수입니다. 작업은 이미 완료되었지만, 다음 예제와 같이 다운로드한 웹 사이트의 길이를 검색하도록 기다립니다. 작업에 오류가 발생하는 경우 awaitAggregateException을 throw하는 Task<TResult>.Result 속성을 읽는 것과 달리 AggregateException에 저장된 첫 번째 자식 예외를 throw합니다.

    total += await finishedTask;
    

프로세스 메서드 추가

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

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를 사용하는 것은 적은 수의 작업이 필요한 문제 해결에 적합합니다. 그러므로 많은 수의 작업을 처리해야 하는 경우에는 다른 접근 방법이 더 효율적입니다. 자세한 내용 및 예제는 작업이 완료되었을 때 처리 방법을 참조하세요.

전체 예제

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

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/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/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

참고 항목