작업 목록 취소

완료될 때까지 기다리지 않으려는 경우 비동기 콘솔 애플리케이션을 취소할 수 있습니다. 이 항목의 예제에 따라 웹 사이트 목록의 콘텐츠를 다운로드하는 애플리케이션에 취소를 추가할 수 있습니다. CancellationTokenSource 인스턴스를 각 작업과 연결하여 많은 작업을 취소할 수 있습니다. Enter 키를 선택하면 아직 완료되지 않은 모든 작업이 취소됩니다.

이 자습서에서는 다음 내용을 다룹니다.

  • .NET 콘솔 애플리케이션 만들기
  • 취소를 지원하는 비동기 애플리케이션 작성
  • 취소 신호 보내기 시연

필수 조건

이 자습서를 사용하려면 다음이 필요합니다.

예제 애플리케이션 만들기

새 .NET Core 콘솔 애플리케이션을 만듭니다. dotnet new console 명령 또는 Visual Studio를 사용하여 만들 수 있습니다. 선호하는 코드 편집기에서 Program.cs 파일을 엽니다.

using 문 바꾸기

기존 using 문을 다음 선언으로 바꿉니다.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

필드 추가

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

static readonly CancellationTokenSource s_cts = new CancellationTokenSource();

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

CancellationTokenSource는 요청된 취소를 CancellationToken에 신호로 보내는 데 사용됩니다. HttpClient는 HTTP 요청을 보내고 HTTP 응답을 받는 기능을 공개합니다. s_urlList는 애플리케이션이 처리해야 하는 모든 URL을 저장합니다.

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

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

static async Task Main()
{
    Console.WriteLine("Application started.");
    Console.WriteLine("Press the ENTER key to cancel...\n");

    Task cancelTask = Task.Run(() =>
    {
        while (Console.ReadKey().Key != ConsoleKey.Enter)
        {
            Console.WriteLine("Press the ENTER key to cancel...");
        }

        Console.WriteLine("\nENTER key pressed: cancelling downloads.\n");
        s_cts.Cancel();
    });
    
    Task sumPageSizesTask = SumPageSizesAsync();

    Task finishedTask = await Task.WhenAny(new[] { cancelTask, sumPageSizesTask });
    if (finishedTask == cancelTask)
    {
        // wait for the cancellation to take place:
        try
        {
            await sumPageSizesTask;
            Console.WriteLine("Download task completed before cancel request was processed.");
        }
        catch (TaskCanceledException)
        {
            Console.WriteLine("Download task has been cancelled.");
        }
    }
        
    Console.WriteLine("Application ending.");
}

업데이트된 Main 메서드는 이제 Async main으로 간주되어 실행 파일에 대한 비동기 진입점을 허용합니다. 콘솔에 몇 가지 지침 메시지를 기록한 다음, cancelTask라는 Task 인스턴스를 선언합니다. 그러면 콘솔 키 입력이 읽힙니다. Enter 키를 누르면 CancellationTokenSource.Cancel()이 호출됩니다. 그러면 취소 신호가 전송됩니다. 다음으로, sumPageSizesTask 변수가 SumPageSizesAsync 메서드에서 할당됩니다. 두 작업은 모두 Task.WhenAny(Task[])에 전달되며 해당 항목은 두 작업 중 하나가 완료되면 계속됩니다.

다음 코드 블록은 취소가 처리될 때까지 애플리케이션이 종료되지 않도록 합니다. 완료할 첫 번째 작업이 cancelTask인 경우 sumPageSizeTask이(가) 대기합니다. 취소된 경우 대기하면 System.Threading.Tasks.TaskCanceledException이(가) 반환됩니다. 블록은 해당 예외를 catch하고 메시지를 출력합니다.

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

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

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

    int total = 0;
    foreach (string url in s_urlList)
    {
        int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
        total += contentLength;
    }

    stopwatch.Stop();

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

Stopwatch를 인스턴스화하고 시작함으로써 메서드가 시작됩니다. 그런 다음, s_urlList의 각 URL을 반복하고 ProcessUrlAsync를 호출합니다. 각 반복에서 s_cts.TokenProcessUrlAsync 메서드에 전달되며 코드는 Task<TResult>를 반환합니다. 여기서 TResult는 정수입니다.

int total = 0;
foreach (string url in s_urlList)
{
    int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
    total += contentLength;
}

프로세스 메서드 추가

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

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

    return content.Length;
}

지정된 URL에서 메서드는 제공된 client 인스턴스를 사용하여 응답을 byte[]로 가져옵니다. CancellationToken 인스턴스는 HttpClient.GetAsync(String, CancellationToken)HttpContent.ReadAsByteArrayAsync() 메서드에 전달됩니다. token은 요청된 취소를 등록하는 데 사용됩니다. URL 및 길이가 콘솔에 기록된 후 길이가 반환됩니다.

예제 애플리케이션 출력

Application started.
Press the ENTER key to cancel...

https://learn.microsoft.com                                       37,357
https://learn.microsoft.com/aspnet/core                           85,589
https://learn.microsoft.com/azure                                398,939
https://learn.microsoft.com/azure/devops                          73,663
https://learn.microsoft.com/dotnet                                67,452
https://learn.microsoft.com/dynamics365                           48,582
https://learn.microsoft.com/education                             22,924

ENTER key pressed: cancelling downloads.

Application ending.

전체 예제

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

using System.Diagnostics;

class Program
{
    static readonly CancellationTokenSource s_cts = new CancellationTokenSource();

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

    static async Task Main()
    {
        Console.WriteLine("Application started.");
        Console.WriteLine("Press the ENTER key to cancel...\n");

        Task cancelTask = Task.Run(() =>
        {
            while (Console.ReadKey().Key != ConsoleKey.Enter)
            {
                Console.WriteLine("Press the ENTER key to cancel...");
            }

            Console.WriteLine("\nENTER key pressed: cancelling downloads.\n");
            s_cts.Cancel();
        });

        Task sumPageSizesTask = SumPageSizesAsync();

        Task finishedTask = await Task.WhenAny(new[] { cancelTask, sumPageSizesTask });
        if (finishedTask == cancelTask)
        {
            // wait for the cancellation to take place:
            try
            {
                await sumPageSizesTask;
                Console.WriteLine("Download task completed before cancel request was processed.");
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("Download task has been cancelled.");
            }
        }

        Console.WriteLine("Application ending.");
    }

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

        int total = 0;
        foreach (string url in s_urlList)
        {
            int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
            total += contentLength;
        }

        stopwatch.Stop();

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

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

        return content.Length;
    }
}

참고 항목

다음 단계