비동기 반환 형식(C#)

비동기 메서드의 반환 형식은 다음과 같을 수 있습니다.

  • Task - 작업을 수행하지만 아무 값도 반환하지 않는 비동기 메서드의 경우
  • Task<TResult> - 값을 반환하는 비동기 메서드의 경우
  • void - 이벤트 처리기의 경우
  • 액세스 가능한 GetAwaiter 메서드가 있는 모든 형식. GetAwaiter 메서드에서 반환된 개체는 System.Runtime.CompilerServices.ICriticalNotifyCompletion 인터페이스를 구현해야 합니다.
  • IAsyncEnumerable<T>- 비동기 스트림을 반환하는 비동기 메서드의 경우입니다.

비동기 메서드에 관한 자세한 내용은 async 및 await를 사용한 비동기 프로그래밍(C#)을 참조하세요.

Windows 워크로드와 관련된 몇 가지 다른 형식도 있습니다.

Task 반환 형식

return 문을 포함하지 않거나 피연산자를 반환하지 않는 return 문을 포함하는 비동기 메서드의 반환 형식은 일반적으로 Task입니다. 이러한 메서드는 동기적으로 실행될 경우 void를 반환합니다. 비동기 메서드에 대해 Task 반환 형식을 사용하는 경우 호출된 비동기 메서드가 완료될 때까지 호출 메서드는 await 연산자를 사용하여 호출자의 완료를 일시 중단할 수 있습니다.

다음 예제에서 WaitAndApologizeAsync 메서드에는 return 문이 없으므로 메서드가 Task 개체를 반환합니다. Task를 반환하면 WaitAndApologizeAsync가 대기할 수 있습니다. Task 형식에는 반환 값이 없으므로 Result 속성이 포함되지 않습니다.

public static async Task DisplayCurrentInfoAsync()
{
    await WaitAndApologizeAsync();

    Console.WriteLine($"Today is {DateTime.Now:D}");
    Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}");
    Console.WriteLine("The current temperature is 76 degrees.");
}

static async Task WaitAndApologizeAsync()
{
    await Task.Delay(2000);

    Console.WriteLine("Sorry for the delay...\n");
}
// Example output:
//    Sorry for the delay...
//
// Today is Monday, August 17, 2020
// The current time is 12:59:24.2183304
// The current temperature is 76 degrees.

동기 void를 반환하는 메서드에 대한 호출 문과 비슷하게 await 식 대신 await 문을 사용하여 WaitAndApologizeAsync가 대기됩니다. 이 경우 await 연산자를 적용하면 값이 산출되지 않습니다. await의 오른쪽 피연산자가 Task<TResult>인 경우 await 식의 결과는 T입니다. await의 오른쪽 피연산자가 Task인 경우 await 및 해당 피연산자는 문입니다.

다음 코드와 같이 WaitAndApologizeAsync 호출을 await 연산자의 적용과 구분할 수 있습니다. 그러나 Task에는 Result 속성이 없으므로 await 연산자가 Task에 적용될 때 값이 생성되지 않습니다.

다음 코드에서는 WaitAndApologizeAsync 메서드 호출과 해당 메서드에서 반환하는 작업 대기를 구분합니다.

Task waitAndApologizeTask = WaitAndApologizeAsync();

string output =
    $"Today is {DateTime.Now:D}\n" +
    $"The current time is {DateTime.Now.TimeOfDay:t}\n" +
    "The current temperature is 76 degrees.\n";

await waitAndApologizeTask;
Console.WriteLine(output);

Task<TResult> 반환 형식

Task<TResult> 반환 형식은 피연산자가 TResultreturn 문이 포함된 비동기 메서드에 사용합니다.

다음 예제에서 GetLeisureHoursAsync 메서드는 정수를 반환하는 return 문을 포함합니다. 메서드 선언은 Task<int>의 반환 형식을 지정해야 합니다. FromResult 비동기 메서드는 DayOfWeek를 반환하는 작업의 자리 표시자입니다.

public static async Task ShowTodaysInfoAsync()
{
    string message =
        $"Today is {DateTime.Today:D}\n" +
        "Today's hours of leisure: " +
        $"{await GetLeisureHoursAsync()}";

    Console.WriteLine(message);
}

static async Task<int> GetLeisureHoursAsync()
{
    DayOfWeek today = await Task.FromResult(DateTime.Now.DayOfWeek);

    int leisureHours =
        today is DayOfWeek.Saturday || today is DayOfWeek.Sunday
        ? 16 : 5;

    return leisureHours;
}
// Example output:
//    Today is Wednesday, May 24, 2017
//    Today's hours of leisure: 5

ShowTodaysInfo 메서드의 await 식 내에서 GetLeisureHoursAsync를 호출하면 await 식이 GetLeisureHours에서 반환된 작업에 저장된 정수 값(leisureHours 값)을 검색합니다. await 식에 대한 자세한 내용은 await를 참조하세요.

다음 코드와 같이 GetLeisureHoursAsync 호출을 await 적용과 구분하면 awaitTask<T>에서 결과를 검색하는 방법을 더욱 잘 이해할 수 있습니다. 곧바로 대기 상태가 되지 않는 GetLeisureHoursAsync 메서드를 호출하면 메서드 선언에서 예상한 대로 Task<int>를 반환합니다. 예제에서 작업이 getLeisureHoursTask 변수에 할당됩니다. getLeisureHoursTaskTask<TResult>이기 때문에 TResult 형식의 Result 속성을 포함합니다. 이 경우 TResult는 정수 형식을 나타냅니다. awaitgetLeisureHoursTask에 적용되는 경우 await 식은 getLeisureHoursTaskResult 속성 내용으로 평가됩니다. 값은 ret 변수에 할당됩니다.

Important

Result 속성은 차단 속성입니다. 해당 작업이 완료되기 전에 액세스하려고 하면, 작업이 완료되고 값을 사용할 수 있을 때까지 현재 활성화된 스레드가 차단됩니다. 대부분의 경우 속성에 직접 액세스하지 않고 await를 사용하여 값에 액세스해야 합니다.

이전 예제에서는 Main 메서드가 애플리케이션 종료 전에 message를 콘솔에 인쇄할 수 있도록 Result 속성의 값을 검색하여 주 스레드를 차단했습니다.

var getLeisureHoursTask = GetLeisureHoursAsync();

string message =
    $"Today is {DateTime.Today:D}\n" +
    "Today's hours of leisure: " +
    $"{await getLeisureHoursTask}";

Console.WriteLine(message);

Void 반환 형식

void반환 형식이 필요한 비동기 이벤트 처리기에 void 반환 형식을 사용합니다. 값을 반환하지 않는 이벤트 처리기 이외의 메서드의 경우 void를 반환하는 비동기 메서드를 대기할 수 없기 때문에 Task를 대신 반환해야 합니다. 해당 메서드의 호출자는 호출된 비동기 메서드가 마치는 것을 기다리지 않고 완료될 때까지 계속 진행해야 합니다. 호출자는 비동기 메서드가 생성하는 모든 값 또는 예외와 독립되어 있어야 합니다.

void를 반환하는 비동기 메서드의 호출자는 메서드에서 throw된 예외를 catch할 수 없습니다. 이러한 처리되지 않은 예외로 인해 애플리케이션이 실패할 수 있습니다. Task 또는 Task<TResult>를 반환하는 메서드가 예외를 throw하는 경우 이 예외는 반환된 작업에 저장됩니다. 작업이 대기하는 경우 예외가 다시 throw됩니다. 예외를 생성할 수 있는 비동기 메서드의 반환 형식이 Task 또는 Task<TResult>이고 메서드 호출이 대기 중인지 확인합니다.

다음 예제에서는 비동기 이벤트 처리기의 동작을 보여줍니다. 예제 코드에서 비동기 이벤트 처리기는 주 스레드가 완료되면 이를 알려야 합니다. 그런 다음, 주 스레드는 비동기 이벤트 처리기가 프로그램을 종료하기 전에 완료될 때까지 대기할 수 있습니다.

public class NaiveButton
{
    public event EventHandler? Clicked;

    public void Click()
    {
        Console.WriteLine("Somebody has clicked a button. Let's raise the event...");
        Clicked?.Invoke(this, EventArgs.Empty);
        Console.WriteLine("All listeners are notified.");
    }
}

public class AsyncVoidExample
{
    static readonly TaskCompletionSource<bool> s_tcs = new TaskCompletionSource<bool>();

    public static async Task MultipleEventHandlersAsync()
    {
        Task<bool> secondHandlerFinished = s_tcs.Task;

        var button = new NaiveButton();

        button.Clicked += OnButtonClicked1;
        button.Clicked += OnButtonClicked2Async;
        button.Clicked += OnButtonClicked3;

        Console.WriteLine("Before button.Click() is called...");
        button.Click();
        Console.WriteLine("After button.Click() is called...");

        await secondHandlerFinished;
    }

    private static void OnButtonClicked1(object? sender, EventArgs e)
    {
        Console.WriteLine("   Handler 1 is starting...");
        Task.Delay(100).Wait();
        Console.WriteLine("   Handler 1 is done.");
    }

    private static async void OnButtonClicked2Async(object? sender, EventArgs e)
    {
        Console.WriteLine("   Handler 2 is starting...");
        Task.Delay(100).Wait();
        Console.WriteLine("   Handler 2 is about to go async...");
        await Task.Delay(500);
        Console.WriteLine("   Handler 2 is done.");
        s_tcs.SetResult(true);
    }

    private static void OnButtonClicked3(object? sender, EventArgs e)
    {
        Console.WriteLine("   Handler 3 is starting...");
        Task.Delay(100).Wait();
        Console.WriteLine("   Handler 3 is done.");
    }
}
// Example output:
//
// Before button.Click() is called...
// Somebody has clicked a button. Let's raise the event...
//    Handler 1 is starting...
//    Handler 1 is done.
//    Handler 2 is starting...
//    Handler 2 is about to go async...
//    Handler 3 is starting...
//    Handler 3 is done.
// All listeners are notified.
// After button.Click() is called...
//    Handler 2 is done.

일반화된 비동기 반환 형식 및 ValueTask<TResult>

비동기 메서드는 awaiter 형식 인스턴스를 반환하는 액세스 가능한GetAwaiter메서드가 있는 모든 형식을 반환할 수 있습니다. 또한 GetAwaiter 메서드가 반환하는 형식에는 System.Runtime.CompilerServices.AsyncMethodBuilderAttribute 특성이 있어야 합니다. 컴파일러가 읽은 특성 또는 작업 유형 작성기 패턴대한 C# 사양에 대한 문서에서 자세히 알아볼 수 있습니다.

이 기능은 await의 피연산자에 대한 요구 사항을 설명하는 대기 가능 식을 보완합니다. 일반화된 비동기 반환 형식을 사용하면 컴파일러가 다른 형식을 반환하는 async 메서드를 생성할 수 있습니다. 일반화된 비동기 반환 형식은 .NET 라이브러리의 성능 향상을 지원합니다. TaskTask<TResult>는 참조 형식이므로 특히 타이트 루프에서 할당이 발생하는 경우 성능이 중요한 경로의 메모리 할당으로 인해 성능이 저하될 수 있습니다. 일반화된 반환 형식이 지원되면 추가 메모리 할당을 방지하기 위해 참조 형식 대신 간단한 값 형식을 반환할 수 있습니다.

.NET에서는 System.Threading.Tasks.ValueTask<TResult> 구조체를 일반화된 작업 반환 값의 간단한 구현으로 제공합니다. 다음 예제에서는 ValueTask<TResult> 구조체를 사용하여 두 주사위 굴리기 값을 검색합니다.

class Program
{
    static readonly Random s_rnd = new Random();

    static async Task Main() =>
        Console.WriteLine($"You rolled {await GetDiceRollAsync()}");

    static async ValueTask<int> GetDiceRollAsync()
    {
        Console.WriteLine("Shaking dice...");

        int roll1 = await RollAsync();
        int roll2 = await RollAsync();

        return roll1 + roll2;
    }

    static async ValueTask<int> RollAsync()
    {
        await Task.Delay(500);

        int diceRoll = s_rnd.Next(1, 7);
        return diceRoll;
    }
}
// Example output:
//    Shaking dice...
//    You rolled 8

일반화된 비동기 반환 형식 작성은 고급 시나리오이며 특수한 환경에서 사용할 수 있습니다. 비동기 코드에 대한 대부분의 시나리오에 적용되는 Task, Task<T>, ValueTask<T> 형식을 대신 사용하는 것이 좋습니다.

C# 10 이상에서는 비동기 반환 형식 선언 대신 비동기 메서드에 AsyncMethodBuilder 특성을 적용하여 해당 형식에 대한 작성기를 재정의할 수 있습니다. 일반적으로 .NET 런타임에 제공된 다른 작성기를 사용하려면 이 특성을 적용합니다.

IAsyncEnumerable<T>를 사용하는 비동기 스트림

비동기 메서드는 IAsyncEnumerable<T>에 의해 나타내는 비동기 스트림을 반환할 수 있습니다. 비동기 스트림은 반복되는 비동기 호출을 통해 요소가 청크로 생성될 때 스트림에서 읽은 항목을 열거하는 방법을 제공합니다. 다음 예제에서는 비동기 스트림을 생성하는 비동기 메서드를 보여 줍니다.

static async IAsyncEnumerable<string> ReadWordsFromStreamAsync()
{
    string data =
        @"This is a line of text.
              Here is the second line of text.
              And there is one more for good measure.
              Wait, that was the penultimate line.";

    using var readStream = new StringReader(data);

    string? line = await readStream.ReadLineAsync();
    while (line != null)
    {
        foreach (string word in line.Split(' ', StringSplitOptions.RemoveEmptyEntries))
        {
            yield return word;
        }

        line = await readStream.ReadLineAsync();
    }
}

앞의 예제에서는 문자열의 줄을 비동기적으로 읽습니다. 각 줄을 읽은 후에는 코드가 문자열에서 각 단어를 열거합니다. 호출자는 await foreach 문을 사용하여 각 단어를 열거합니다. 메서드는 소스 문자열에서 다음 줄을 비동기적으로 읽어야 하는 경우 대기합니다.

참고 항목