작업 비동기 프로그래밍 모델

비동기 프로그래밍을 사용하여 성능 병목 현상을 방지하고 애플리케이션의 전체적인 응답성을 향상할 수 있습니다. 그러나 비동기 애플리케이션을 쓰는 일반적인 기술이 복잡하여 해당 애플리케이션을 쓰고, 디버깅하고, 유지 관리하기 어려울 수 있습니다.

C#은 .NET 런타임에 비동기 지원을 활용하는 간단한 비동기 프로그래밍을 새로 도입했습니다. 컴파일러는 개발자가 하던 어려운 작업을 수행하고, 애플리케이션은 동기 코드와 비슷한 논리 구조를 유지합니다. 따라서 약간의 노력만으로도 비동기 프로그래밍의 모든 장점을 누릴 수 있습니다.

이 항목에서는 비동기 프로그래밍을 사용하는 시기 및 방법에 대한 개요를 제공하고 특정 세부 정보 및 예제가 포함된 지원 항목에 대한 링크가 포함되어 있습니다.

반응성을 향상시키는 비동기

비동기는 웹 액세스와 같이 차단 가능성이 있는 작업에 반드시 필요합니다. 웹 리소스에 대한 액세스 속도가 느리거나 지연됩니다. 동기 프로세스 안에서 이러한 활동이 차단되면 전체 애플리케이션이 기다려야 합니다. 비동기 프로세스에서 애플리케이션은 잠재적인 차단 작업이 완료될 때까지 웹 리소스에 의존하지 않는 다른 작업을 계속 수행할 수 있습니다.

다음 표에는 비동기 프로그래밍으로 응답성이 향상되는 일반적인 영역이 나와 있습니다. .NET 및 Windows 런타임에서 나열된 API에는 비동기 프로그래밍을 지원하는 메서드가 포함되어 있습니다.

애플리케이션 영역 비동기 메서드가 있는 .NET 형식 비동기 메서드가 있는 Windows 런타임 형식
웹 액세스 HttpClient Windows.Web.Http.HttpClient
SyndicationClient
파일 작업 JsonSerializer
StreamReader
StreamWriter
XmlReader
XmlWriter
StorageFile
이미지 작업 MediaCapture
BitmapEncoder
BitmapDecoder
WCF 프로그래밍 동기 및 비동기 작업

모든 UI 관련 작업이 대체로 스레드 한 개를 공유하므로 비동기는 특히 UI 스레드에 액세스하는 애플리케이션의 변수를 증명합니다. 동기 애플리케이션에서 임의의 프로세스가 차단되면 모든 프로세스가 차단됩니다. 애플리케이션이 응답을 중지하면 기다리지 않고 애플리케이션이 실패한 것으로 결론을 내릴 것입니다.

비동기 메서드를 사용하면 애플리케이션이 UI에 계속 응답합니다. 예를 들어 창의 크기를 조정하거나 최소화할 수 있습니다. 또는 애플리케이션이 완료될 때까지 기다리고 싶지 않다면 애플리케이션을 종료할 수 있습니다.

비동기 기반 접근 방식을 사용하면 비동기 작업을 디자인할 때 선택할 수 있는 옵션 목록에 자동 전송과 동일한 기능을 추가할 수 있습니다. 즉, 더 적은 개발자의 노력으로 기존 비동기 프로그래밍의 이점을 모두 활용할 수 있습니다.

작성이 간편한 비동기 메서드

C#의 asyncawait 키워드는 비동기 프로그래밍의 핵심입니다. 이 두 개의 키워드를 사용하면 .NET Framework, .NET Core 또는 Windows 런타임의 리소스를 사용하여 동기 메서드를 만드는 것만큼 쉽게 비동기 메서드를 만들 수 있습니다. async 키워드를 사용하여 정의하는 비동기 메서드를 비동기 메서드라고 합니다.

다음 예제에서는 비동기 메서드를 보여줍니다. 코드의 거의 모든 내용이 익숙할 것입니다.

async 및 await를 사용한 비동기 프로그래밍(C#)에서 다운로드 가능한 전체 WPF(Windows Presentation Foundation) 예제를 찾을 수 있습니다.

public async Task<int> GetUrlContentLengthAsync()
{
    var client = new HttpClient();

    Task<string> getStringTask =
        client.GetStringAsync("https://learn.microsoft.com/dotnet");

    DoIndependentWork();

    string contents = await getStringTask;

    return contents.Length;
}

void DoIndependentWork()
{
    Console.WriteLine("Working...");
}

위의 샘플에서 몇 가지 사례를 알아볼 수 있습니다. 메서드 서명부터 시작해봅시다. 여기에는 async 한정자가 포함됩니다. 반환 형식은 Task<int>입니다(추가 옵션은 "반환 형식" 섹션 참조). 메서드 이름은 Async로 끝납니다. 메서드의 본문에서 GetStringAsyncTask<string>을 반환합니다. 즉, 작업을 await하는 경우 string을 받게 됩니다(contents). 작업을 대기하기 전에 GetStringAsyncstring을 사용하지 않는 작업을 수행할 수 있습니다.

await 연산자에 주의하세요. GetUrlContentLengthAsync를 일시 중단합니다.

  • GetUrlContentLengthAsyncgetStringTask가 완료될 때까지 계속할 수 없습니다.
  • 반면 제어는 GetUrlContentLengthAsync의 호출자에 반환됩니다.
  • getStringTask가 완료되면 컨트롤이 다시 시작됩니다.
  • 그런 다음, await 연산자가 getStringTask에서 string 결과를 검색합니다.

반환 문은 정수 결과를 지정합니다. GetUrlContentLengthAsync를 대기하는 메서드는 길이 값을 검색합니다.

GetUrlContentLengthAsyncGetStringAsync 호출과 해당 완료 대기 사이에 수행할 수 있는 작업이 없는 경우 다음 단일 문을 호출하고 대기하여 코드를 단순화할 수 있습니다.

string contents = await client.GetStringAsync("https://learn.microsoft.com/dotnet");

이전 예제가 비동기 메서드인 이유는 다음과 같은 특성 때문입니다.

  • 메서드 시그니처에 async 한정자가 포함됩니다.

  • 비동기 메서드의 이름은 규칙에 따라 "Async" 접미사로 끝납니다.

  • 반환 형식은 다음 형식 중 하나입니다.

    • 메서드에 연산자 형식이 TResult인 Return 문이 있는 경우 Task<TResult>입니다.
    • 메서드에 반환 문이 포함되지 않았거나 피연산자가 없는 반환 문이 포함된 경우 Task입니다.
    • 비동기 이벤트 처리기를 작성하는 경우 void입니다.
    • GetAwaiter 메서드가 포함된 모든 기타 형식.

    자세한 내용은 반환 형식 및 매개 변수 섹션을 참조하세요.

  • 메서드는 일반적으로 비동기 작업이 완료될 때까지 메서드가 계속될 수 없는 지점을 표시하는 하나 이상의 await 표현을 포함하고 있습니다. 한편, 메서드가 일시 중단되고 컨트롤이 메서드의 호출자로 반환됩니다. 이 항목의 다음 단원은 일시 중단 지점에서 발생하는 상황을 보여줍니다.

비동기 메서드에서는 제공된 키워드 및 형식을 사용해서 수행하려는 작업을 나타내고, 컴파일러는 일시 중단된 메서드에서 컨트롤이 대기 지점으로 반환될 때 수행되어야 하는 항목을 추적하는 등의 나머지 작업을 수행합니다. 루프 및 예외 처리와 같은 일부 루틴 프로세스는 기존의 비동기 코드에서 처리하기 어려울 수 있습니다. 비동기 메서드에서는 동기 솔루션에서와 같이 필요한 만큼 이러한 요소를 작성하여 문제를 해결합니다.

이전 버전의 .NET Framework의 비동기에 관한 자세한 내용은 TPL 및 일반적인 .NET Framework 비동기 프로그래밍을 참조하세요.

비동기 메서드에서 수행되는 작업

비동기 프로그래밍을 이해하는 데 있어 가장 중요한 점은 메서드에서 메서드로 제어 흐름을 이동하는 방법입니다. 다음 다이어그램에서 과정을 안내합니다.

Trace navigation of async control flow

다이어그램의 숫자는 호출하는 메서드가 비동기 메서드를 호출할 때 시작되는 다음 단계에 해당합니다.

  1. 호출하는 메서드는 GetUrlContentLengthAsync 비동기 메서드를 호출하고 기다립니다.

  2. GetUrlContentLengthAsyncHttpClient 인스턴스를 만들고 GetStringAsync 비동기 메서드를 호출하여 웹 사이트의 내용을 문자열로 다운로드합니다.

  3. GetStringAsync에서 특정 작업이 발생하여 진행이 일시 중단됩니다. 웹 사이트에서 다운로드 또는 다른 차단 작업을 수행할 때까지 기다려야 할 수 있습니다. 리소스를 차단하지 않기 위해 GetStringAsync는 해당 호출자인 GetUrlContentLengthAsync에 제어 권한을 양도합니다.

    GetStringAsyncTResult가 문자열인 Task<TResult>를 반환하고, GetUrlContentLengthAsyncgetStringTask 변수에 작업을 할당합니다. 이 작업은 작업이 완료될 때 실제 문자열 값을 생성하기 위한 코드와 함께 GetStringAsync를 호출하는 지속적인 프로세스를 나타냅니다.

  4. getStringTask가 아직 대기되지 않았으므로 GetUrlContentLengthAsyncGetStringAsync의 최종 결과에 무관한 다른 작업을 계속할 수 있습니다. 이 작업은 동기 메서드 DoIndependentWork를 호출하여 나타냅니다.

  5. DoIndependentWork는 작업을 수행하고 호출자에게 반환하는 동기 메서드입니다.

  6. GetUrlContentLengthAsyncgetStringTask 결과 없이 수행할 수 있는 작업이 없습니다. 다음으로 GetUrlContentLengthAsync는 다운로드한 문자열의 길이를 계산하여 반환하려 하지만, 메서드가 문자열을 확인할 때까지 해당 값을 계산할 수 없습니다.

    따라서 GetUrlContentLengthAsync는 await 연산자를 사용해서 해당 프로세스를 일시 중단하고 GetUrlContentLengthAsync를 호출한 메서드에 제어 권한을 양도합니다. GetUrlContentLengthAsyncTask<int>를 호출자에게 반환합니다. 작업은 다운로드한 문자열의 길이인 정수 결과를 만든다는 약속을 나타냅니다.

    참고

    GetUrlContentLengthAsync가 대기하기 전에 GetStringAsync(및 getStringTask)가 완료되면 GetUrlContentLengthAsync에서 컨트롤이 유지됩니다. 호출된 비동기 프로세스 getStringTask가 이미 완료되었고 GetUrlContentLengthAsync가 최종 결과를 기다릴 필요가 없다면 일시 중단한 다음, GetUrlContentLengthAsync로 돌아가는 비용이 낭비됩니다.

    호출하는 메서드 내에서 패턴 처리가 계속됩니다. 호출자가 해당 결과를 기다리거나 즉시 기다리기 전에 GetUrlContentLengthAsync에서 결과에 의존하지 않는 다른 작업을 수행할 수 있습니다. 호출하는 메서드는 GetUrlContentLengthAsync를 기다리고 있으며 GetUrlContentLengthAsyncGetStringAsync를 기다리고 있습니다.

  7. GetStringAsync가 완료되고 문자열 결과를 생성합니다. GetStringAsync를 호출할 경우 문자열 결과가 예상대로 반환되지 않습니다. (메서드가 이미 3단계에서 작업을 반환했습니다.) 대신 문자열 결과가 메서드 getStringTask의 완료를 나타내는 작업에 저장됩니다. await 연산자가 getStringTask에서 결과를 검색합니다. 할당 문은 검색된 결과를 contents에 할당합니다.

  8. GetUrlContentLengthAsync에 문자열 결과가 있는 경우 메서드가 문자열 길이를 계산할 수 있습니다. 그런 다음 GetUrlContentLengthAsync 작업도 완료되고 대기 이벤트 처리기를 다시 시작할 수 있습니다. 이 항목 뒷부분의 전체 예에서는 이벤트 처리기가 길이 결과 값을 검색하고 출력하는지 확인할 수 있습니다. 비동기 프로그래밍을 처음 접하는 사용자인 경우 동기 동작과 비동기 동작의 차이점을 살펴보세요. 동기 메서드는 작업이 완료될 때 반환되지만(5단계) 비동기 메서드는 작업이 일시 중단될 때 반환됩니다.(3~6단계) 비동기 메서드가 해당 작업을 완료하면 작업이 완료된 것으로 표시되고 결과가 있을 경우 작업에 저장됩니다.

API 비동기 메서드

GetStringAsync와 같이 비동기 프로그래밍을 지원하는 메서드를 어디에서 검색해야 할지 궁금했을 것입니다. .NET Framework 4.5 이상 및 .NET Core에는 asyncawait에 작동하는 많은 멤버가 포함되어 있습니다. 멤버 이름에 붙는 “Async” 접미사와 Task 또는 Task<TResult>의 반환 형식으로 멤버를 인식할 수 있습니다. 예를 들어, System.IO.Stream 클래스는 동기 메서드인 CopyTo, ReadWrite와 함께 CopyToAsync, ReadAsyncWriteAsync와 같은 메서드를 포함합니다.

Windows 런타임에는 Windows 앱에서 asyncawait와 함께 사용할 수 있는 많은 메서드도 포함되어 있습니다. 자세한 내용은 UWP 개발의 경우 스레딩 및 비동기 프로그래밍을 참조하세요. Windows 런타임 이전 버전을 사용하는 경우 비동기 프로그래밍(Windows 스토어 앱)빠른 시작: C# 또는 Visual Basic에서 비동기 API 호출을 참조하세요.

스레드

비동기 메서드는 비차단 작업으로 의도되었습니다. 비동기 메서드의 await 식은 대기한 작업이 실행되는 동안 현재 스레드를 차단하지 않습니다. 대신에 이 식은 메서드의 나머지를 연속으로 등록하고 제어 기능을 비동기 메서드 호출자에게 반환합니다.

asyncawait 키워드로 인해 추가 스레드가 생성되지 않습니다. 비동기 메서드는 자체 스레드에서 실행되지 않으므로 다중 스레드가 필요하지 않습니다. 메서드는 현재 동기화 컨텍스트에서 실행되고 메서드가 활성화된 경우에만 스레드에서 시간을 사용합니다. Task.Run을 사용하여 CPU 바인딩 작업을 백그라운드 스레드로 이동할 수 있지만 백그라운드 스레드는 결과를 사용할 수 있을 때까지 기다리는 프로세스를 도와주지 않습니다.

비동기 프로그래밍에 대한 비동기 기반 접근 방법은 거의 모든 경우에 기존 방법보다 선호됩니다. 특히, 이 접근 방식은 코드가 더 간단하고 경합 조건을 방지할 필요가 없기 때문에 I/O 바인딩 작업의 BackgroundWorker 클래스보다 효과적입니다. 비동기 프로그래밍은 코드 실행에 대한 조합 세부 정보를 Task.Run이 스레드 풀로 변환하는 작업과 구분하기 때문에 Task.Run 메서드를 함께 사용하는 비동기 프로그래밍은 CPU 바인딩 작업을 위한 BackgroundWorker보다 효과가 뛰어납니다.

Async 및 Await

async 한정자를 사용해서 메서드를 비동기 메서드로 지정하면 다음 두 기능이 활성화됩니다.

  • 표시된 비동기 메서드는 Await를 사용하여 일시 중단 지점을 지정할 수 있습니다. await 연산자는 대기된 비동기 프로세스가 완료될 때까지 비동기 메서드가 해당 지점을 지나 계속할 수 없도록 컴파일러에 지시합니다. 한편, 컨트롤이 비동기 메서드의 호출자로 반환됩니다.

    await 식에서 비동기 메서드를 일시 중단하더라도 메서드가 종료되지는 않으며 finally 블록이 실행되지 않습니다.

  • 표시된 비동기 메서드는 이 메서드를 호출한 다른 메서드에 의해 대기할 수 있습니다.

비동기 메서드는 일반적으로 await 연산자를 하나 이상 가지고 있지만, await 식이 없는 경우 컴파일러 오류가 발생하지는 않습니다. 비동기 메서드에서 await 연산자를 사용하여 일시 중단 시점을 표시하지 않는 경우 메서드가 async 한정자에 상관없이 동기 메서드가 실행되는 방식으로 실행됩니다. 컴파일러는 해당 메서드에 대해 경고를 표시합니다.

asyncawait은 상황별 키워드입니다. 자세한 내용과 예제는 다음 항목을 참조하세요.

반환 형식 및 매개 변수

비동기 메서드는 일반적으로 Task 또는 Task<TResult>를 반환합니다. 비동기 메서드 내에서 await 연산자는 호출에서 다른 비동기 메서드로 전환되는 작업에 적용됩니다.

메서드에 TResult 형식의 피연산자를 지정하는 return 문이 포함되어 있을 경우 Task<TResult>를 반환 형식으로 지정합니다.

메서드에 return 문이 없거나 피연산자를 반환하지 않는 return 문이 있을 경우 반환 형식으로 Task를 사용합니다.

형식에 GetAwaiter 메서드가 포함된 경우 다른 반환 형식을 지정할 수도 있습니다. ValueTask<TResult>가 이러한 형식의 예입니다. System.Threading.Tasks.Extension NuGet 패키지에서 사용할 수 있습니다.

다음 예제는 Task<TResult> 또는 Task를 반환하는 메서드를 선언하고 호출하는 방법을 보여줍니다.

async Task<int> GetTaskOfTResultAsync()
{
    int hours = 0;
    await Task.Delay(0);

    return hours;
}


Task<int> returnedTaskTResult = GetTaskOfTResultAsync();
int intResult = await returnedTaskTResult;
// Single line
// int intResult = await GetTaskOfTResultAsync();

async Task GetTaskAsync()
{
    await Task.Delay(0);
    // No return statement needed
}

Task returnedTask = GetTaskAsync();
await returnedTask;
// Single line
await GetTaskAsync();

반환된 각 작업은 진행 중인 작업을 나타냅니다. 작업은 비동기 프로세스 상태에 대한 정보를 캡슐화하며, 결과적으로 프로세스의 최종 결과 또는 성공하지 못한 경우 프로세스가 발생시키는 예외에 대한 정보를 캡슐화합니다.

비동기 메서드의 반환 형식은 void일 수 있습니다. 이 반환 형식은 기본적으로 void 반환 형식이 필요할 때 이벤트 처리기를 정의하는 데 사용합니다. 비동기 이벤트 처리기는 비동기 프로그램의 시작점 역할을 하는 경우가 많습니다.

void 반환 형식을 가진 비동기 메서드는 대기할 수 없습니다. 또한 void를 반환하는 메서드의 호출자는 메서드가 throw하는 예외를 catch할 수 없습니다.

비동기 메서드는 모든 in, ref 또는 out 매개 변수를 선언할 수 없지만, 이러한 매개 변수가 있는 메서드를 호출할 수는 있습니다. 마찬가지로 비동기 메서드는 참조 반환 값을 사용하여 메서드를 호출할 수 있지만 참조를 통해 값을 반환할 수 없습니다.

자세한 내용과 예제는 비동기 반환 형식(C#)을 참조하세요.

Windows 런타임 프로그래밍의 비동기 API에는 작업과 유사한 다음 반환 형식 중 하나가 있습니다.

명명 규칙

규칙에 따라 일반적으로 대기 가능한 형식(예: Task, Task<T>, ValueTask, ValueTask<T>)을 반환하는 메서드에는 “Async”로 끝나는 이름을 사용해야 합니다. 비동기 작업을 시작하지만 대기 가능한 형식을 반환하지 않는 메서드는 "Async"로 끝나는 이름을 사용하지 않아야 하지만, "Begin", "Start" 또는 일부 다른 동사로 시작하여 이 메서드가 작업 결과를 반환하거나 예외가 발생하지 않음을 알려야 합니다.

여기서 이벤트, 기본 클래스 또는 인터페이스 계약으로 다른 이름을 제안하는 규칙을 무시할 수 있습니다. 예를 들어, OnButtonClick과 같은 공용 이벤트 처리기의 이름을 변경할 수 없습니다.

관련 문서(Visual Studio)

제목 Description
async 및 await를 사용하여 병렬로 여러 웹 요청을 만드는 방법(C#) 동시에 여러 작업을 시작하는 방법을 보여줍니다.
비동기 반환 형식(C#) 비동기 메서드에서 반환할 수 있는 형식을 설명하고 각 형식이 언제 적절한가를 설명합니다.
신호 메커니즘으로 취소 토큰이 있는 작업을 취소합니다. 비동기 솔루션에 다음과 같은 기능을 추가하는 방법을 보여줍니다.

- 작업 목록 취소(C#)
- 일정 기간 이후 작업 취소(C#)
- 완료되면 비동기 작업 처리(C#)
파일 액세스에 async 사용(C#) async 및 await를 사용하여 파일에 액세스하는 이점을 나열하고 보여줍니다.
TAP(작업 기반 비동기 패턴) 비동기 패턴에 대해 설명하고 패턴은 TaskTask<TResult> 형식을 기반으로 합니다.
비동기 Channel 9 비디오 비동기 프로그래밍에 대한 다양한 비디오로 연결되는 링크를 제공합니다.

참조