완료되면 비동기 작업 처리(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
루프는 컬렉션의 각 작업에서 다음 단계를 수행합니다.
다운로드를 완료한 컬렉션에서 첫 번째 작업을 식별하기 위해
WhenAny
호출을 기다립니다.Task<int> finishedTask = await Task.WhenAny(downloadTasks);
컬렉션에서 해당 작업을 제거합니다.
downloadTasks.Remove(finishedTask);
ProcessUrlAsync
호출에서 반환된finishedTask
를 대기합니다.finishedTask
변수는 Task<TResult>입니다. 여기서TResult
은 정수입니다. 작업은 이미 완료되었지만, 다음 예제와 같이 다운로드한 웹 사이트의 길이를 검색하도록 기다립니다. 작업에 오류가 발생하는 경우await
는AggregateException
을 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
참고 항목
.NET