[C#] 비동기식 프로그래밍 TAP

TAP(Task based Asynchronous Programming)

마이크로소프트 Docs 문서 를 바탕으로 내용 정리하였습니다.

동기식 프로그래밍 vs 비동기식 프로그래밍

코드는 일련의 명령문으로 이루어집니다. 다음 명령문이 시작되기 전에 각 명령문이 완료되는 구조입니다.

  • 동기식 프로그래밍은 스레드에서 작업을 실행하는 동안 다른 작업을 수행하지 못하도록 차단합니다.
  • 마치 어떤 데이터를 다운받는 동안 그 데이터를 다운받는 것을 지켜보며 어플리케이션이 멈춰있는 것처럼 보이는 것이죠.
  • 비동기식 프로그래밍은 외부 리소스 할당과 작업 완료 시점에 따라 복잡한 순서로 코드를 사용하도록 설정합니다.
  • 어떤 데이터를 다운받는 동안에도 어플리케이션이 다른 동작을 하도록 멈추지 않는 것이죠.

위의 예시처럼, 클라이언트 프로그램에서 UI는 사용자의 입력에 응답해야 합니다. 이를 위해 비동기식 프로그래밍이 필요합니다. C#에서는 비동기식 프로그램을 위한 두가지 키워드(await , async )를 제공합니다.

  • async : 먼저 비동기식 동작이 있다는 것을 컴파일러에게 알려주는 키워드입니다.
  • await : await 키워드는 작업을 차단하지 않는 방식으로 시작한 다음, 해당 작업이 완료되면 실행을 계속합니다. 이는 동기식 프로그래밍과 차이가 없어보이지만, 기다리고 있는 스레드가 차단되지 않습니다. 즉, 사용자에게 응답할 수 있는 상황인 것입니다.

동시작업 (Task)

System.Threading.Tasks.Task 클래스를 사용하여 각각에 필요한 작업이 있으므로 해당 작업에 주의를 기울이고, 다음 작업을 처리한 다음, 주의가 필요한 다른 작업을 기다립니다.

  1. 작업시작

    해당 작업을 나타내는 Task 개체 저장.

  2. 작업을 기다리지 않고 다음 코드 실행

  3. 작업의 결과가 필요할 때 await 문 사용

    해당 작업이 끝날 때까지 기다림

Task 또는 Task<TResult> 개체를 사용하여 모든 비동기 작업을 한 번에 시작하고 결과가 필요할 때 각 작업을 기다리고(await), 이 과정에서 다른 작업과 결합하는 작업 구성이 가능합니다.

비동기 반환 형식

  • Task : 작업이 끝난다는 사실만을 알려줄 수 있으나 값은 반환하지 않는 비동기 메서드. await 를 사용할 수 있습니다.
  • Task<TResult> : 값, 객체 등을 반환하는 비동기 메서드. await 에서 받아주도록 사용할 수 있습니다.
  • void : await 를 사용할 수 없으며 그냥 실행하도록 내버려 두는 이벤트 처리기입니다.
  • C# 8.0부터 ‘비동기 스트림’을 반환하는 비동기 메서드의 경우 IAsyncEnumerable.

Async fire and forget

호출한 스레드로부터 응답을 기다리지 않고 process flow를 유지한채 다른 스레드를 호출하는 것을 fire and forget pattern 이라고 합니다.

void return type 을 사용하는 비동기 메서드는 메서드의 실행이 완료될 때까지 기다렸다가 그에 따라 응답할 수 없기 때문에 이벤트 핸들러에 가장 제한된 fire and discount 메서드입니다. 이 방법에서 벗어난 예외를 포착할 수 있는 방법도 없습니다.

따라서 이벤트 핸들러가 아닌 비동기 메소드가 있으면 프로그램이 작동하지만 타이밍 문제로 인해 작동하지 않을 수 있습니다. 이 경우 비동기 메서드는 Task를 반환해야 합니다.

Example

using System;
using System.Threading.Tasks;

namespace Async
{
    class Program
    {
        class Data
        {
            private string data { get; set; }
            public Data()
            {
                data = string.Empty;
            }

            public Data(string input)
            {
                data = input;
            }

            public void PrintData()
            {
                Console.WriteLine(data);
            }
        }

        public static void Main(string[] args)
        {
            AsyncTest();
        }

        private static async Task AsyncTest()
        {
            Task<Data> dataTask1 = GetData1();
            Print();

            Data my_data1 = await dataTask1;
            my_data1.PrintData();

            Task dataTask2 = SingSong();
            Print();
            await dataTask2;
            Print();
        }

        private static void Print()
        {
            Console.WriteLine("=================================");
        }

        static async Task<Data> GetData1(string input= "This is Data")
        {
            Console.WriteLine("Data Downloading...............");
            return new Data(input);
        }

        static async Task SingSong()
        {
            Task sing1 = Sing("1111111");
            Task sing2 = Sing("2222222");
            await sing1;
            await sing2;
        }

        static async Task Sing(string input)
        {
            Console.WriteLine(input);
        }
    }
}
  1. GetData1 비동기 함수가 실행되며 데이터 다운로딩이 시작됩니다.
  2. Print() 동기 함수가 실행되고, GetData1 함수가 끝나길 await 합니다.
  3. 끝난 결과로 Data 객체가 생성되어 my_data1 에 담기고 메서드를 사용해 데이터를 출력합니다.
  4. SingSong 비동기 함수안에서 sing1, sing2 비동기 메서드가 실행되면서 동시에 Print() 동기 함수가 실행됩니다.
  5. Print() 동기 함수가 끝나면 두 메서드가 끝나길 순차적으로 대기하며 완료 후 비동기함수가 종료됩니다.
  6. 마지막으로 Print() 동기 함수가 실행되며 프로그램이 종료됩니다.

비동기 예외

비동기 작업에서도 작업이 성공적으로 완료되지 못하는 경우에 대해서 예외를 throw 하도록 설계할 수 있습니다.

Task 를 리턴하는 작업에서 Exception이 throw 하면 해당 Task는 오류상태 가 됩니다. Task.Exception 속성에서 throw된 예외를 포함하고, 오류 상태인 작업이 대기되면 예외를 throw 합니다.

여기에 중요한 두 가지 메커니즘이 있습니다.

  1. 예외가 오류 상태인 작업에 저장되는 방식
  2. 코드가 오류 상태인 작업을 대기할 때 예외가 패키지 해제되었다가 다시 throw되는 방식

비동기적으로 실행되는 코드가 예외를 throw하면 해당 예외는 Task에 저장됩니다. 비동기작업 중에는 둘 이상의 예외가 throw될 수 있으므로 Task.Exception 속성은 System.AggregateException 입니다. throw된 모든 예외는 AggregateException.InnerExceptions 컬렉션에 추가됩니다.

다시 이해 필요

효율적인 작업 대기

  • Task 클래스의 메서드 WhenAll (API 중 하나)
    • 일련의 await 문을 대신해, 인수 목록의 모든 작업이 완료되면 완료된 Task를 반환
  • WhenAny 메서드
    • 인수가 완료되면 완료된 Task<Task>를 반환
    • 완료된 작업의 결과가 처리되면 완료된 작업을 WhenAny에 전달된 작업 목록에서 제거

'SW > C# & WPF' 카테고리의 다른 글

[C#] Callback, Delegate  (0) 2021.06.01
[WPF] Thread와 Dispatcher를 이용한 멀티스레딩  (0) 2021.06.01
[C#] HTTP 통신  (0) 2021.06.01
[C#] 싱글톤패턴  (0) 2021.05.28
[C#] 추상클래스 & 인터페이스  (0) 2021.05.28