[C#] 40. Task 클래스와 async, await 사용법


Study/C#  2021. 10. 1. 11:01

안녕하세요. 명월입니다.


이 글은 C#에서 Task 클래스와 async, await 사용법에 대한 글입니다.


제가 이전 글에서 Thread에 대해서 설명한 적이 있습니다.

링크 - [C#] 36. 스레드(Thread)를 사용하는 방법, Thread.Sleep 함수 사용법


쓰레드라는 건 병렬 처리라는 건 몇 번을 설명했는지, 이번 글에서는 생략하겠습니다.

Thread를 생성할 때는 시스템 상에서 여러가지 리소스를 사용하고 지나치게 많이 사용되면 반대로 시스템의 성능이 느려집니다. 그래서 쓰레드 풀을 생성해서 쓰레드의 개수 제한과 쓰레드 리소스의 재활용을 통해서 시스템의 성능을 향상 시킬수 있는데 스레드 상태를 제어할 수 없어서 쓰레드가 종료할 때까지 기다리는 것을 별도로 구현을 해야 하는 불편함이 있습니다.

Task는 ThreadPool 안에서 움직이는 쓰레드이고 Thread처럼 쉽게 생성하고 Join 기능까지 사용할 수 있는 기능이 있습니다.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
  // 쓰레드로 넘기는 파라미터 클래스
  class Node
  {
    // 콘솔 출력시 사용되는 텍스트
    public string Text { get; set; }
    // 반복문 횟수
    public int Count { get; set; }
    // Sleep의 시간틱
    public int Tick { get; set; }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // ThreadPool의 최소 쓰레드는 0, 최대 쓰레드는 2개로 설정
      ThreadPool.SetMinThreads(0, 0);
      ThreadPool.SetMaxThreads(2, 0);
      // 리스트에 Task를 설정한다.
      var list = new List<Task<int>>();
      // Task에 사용될 람다식 입력 값은 object 반환값은 int형
      var func = new Func<object, int>((x) =>
      {
        // object를 Node타입으로 강제 캐스팅
        var node = (Node)x;
        // 변수
        int sum = 0;
        // 설정된 반복문의 횟수만큼
        for (int i = 0; i <= node.Count; i++)
        {
          // 값을 더하기
          sum += i;
          // 콘솔 출력
          Console.WriteLine(node.Text + " = " + i);
          // 설정된 Sleep 시간틱
          Thread.Sleep(node.Tick);
        }
        // 콘솔 출력
        Console.WriteLine("Completed " + node.Text);
        return sum;
      });
      // 리스트에 Tack 추가
      list.Add(new Task<int>(func, new Node { Text = "A", Count = 5, Tick = 1000 }));
      list.Add(new Task<int>(func, new Node { Text = "B", Count = 5, Tick = 10 }));
      list.Add(new Task<int>(func, new Node { Text = "C", Count = 5, Tick = 500 }));
      list.Add(new Task<int>(func, new Node { Text = "D", Count = 5, Tick = 300 }));
      list.Add(new Task<int>(func, new Node { Text = "E", Count = 5, Tick = 200 }));
      // list에 넣은 Task를 실행
      list.ForEach(x => x.Start());
      // list에 넣은 Task를 종료될 때까지 실행
      list.ForEach(x => x.Wait());
      // 쓰레드의 합을 출력
      Console.WriteLine("Sum = " + list.Sum(x => x.Result));
      // 아무 키나 누르면 종료
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}

위 결과를 보면 먼저 ThreadPool에서 설정하는 쓰레드 제한 설정이 Task로 선언한 쓰레드에도 영향이 가는 것을 확인할 수 있습니다.

즉, 구현은 Thread 처럼 간단하게 사용할 수 있지만, 내용은 ThreadPool에서 움직이는 것을 확인 할 수 있습니다.


그리고 ThreadPool과 다르게 return 값을 받을 수 있어서 1부터 5까지 더하면 15, 쓰레드가 5개이니깐 총합의 75의 결과가 나오는 것을 확인할 수 있습니다.

lock를 사용하지 않아도 각 쓰레드에서 결과 값을 받아서 각 메인 프로세스에서 쓰레드의 값을 받아 사용할 수 있습니다.


그리고 Task의 다른 기능은 async, await 키워드와 매우 밀접한 관계가 있습니다.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
  // 쓰레드로 넘기는 파라미터 클래스
  class Node
  {
    // 콘솔 출력시 사용되는 텍스트
    public string Text { get; set; }
    // 반복문 횟수
    public int Count { get; set; }
    // Sleep의 시간틱
    public int Tick { get; set; }
  }
  class Program
  {
    private static async Task<int> RunAsync(Node node)
    {
      var task = new Task<int>(() =>
      {
        // 변수
        int sum = 0;
        // 설정된 반복문의 횟수만큼
        for (int i = 0; i <= node.Count; i++)
        {
          // 값을 더하기
          sum += i;
          // 콘솔 출력
          Console.WriteLine(node.Text + " = " + i);
          // 설정된 Sleep 시간틱
          Thread.Sleep(node.Tick);
        }
        // 콘솔 출력
        Console.WriteLine("Completed " + node.Text);
        return sum;
      });
      // Task 실행
      task.Start();
      // task가 종료될 때까지 대기
      await task;
      // task의 결과 리턴
      return task.Result;
    }
    // 실행 함수
    static void Main(string[] args)
    {
      // ThreadPool의 최소 쓰레드는 0, 최대 쓰레드는 2개로 설정
      ThreadPool.SetMinThreads(0, 0);
      ThreadPool.SetMaxThreads(2, 0);
      // 리스트에 Task를 설정한다.
      var list = new List<Task<int>>();
      // 리스트에 Tack 추가
      list.Add(RunAsync(new Node { Text = "A", Count = 5, Tick = 1000 }));
      list.Add(RunAsync(new Node { Text = "B", Count = 5, Tick = 10 }));
      list.Add(RunAsync(new Node { Text = "C", Count = 5, Tick = 500 }));
      list.Add(RunAsync(new Node { Text = "D", Count = 5, Tick = 300 }));
      list.Add(RunAsync(new Node { Text = "E", Count = 5, Tick = 200 }));
      // 쓰레드의 합을 출력
      Console.WriteLine("Sum = " + list.Sum(x => x.Result));
      // 아무 키나 누르면 종료
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}

첫번째 예제와 결과는 같은 결과입니다만, Task를 다루기 쉽게 구현 되었습니다.

async가 선언된 함수에서 Task를 생성하고 실행하고, await으로 쓰레드가 종료될 때까지 대기합니다.

그리고 결과를 리턴하게 되면 Main 프로세스에서 결과를 합산하여 나오는 것을 확인할 수 있습니다.


여기까지가 Task와 async, await의 기본 구조입니다.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
  // 쓰레드로 넘기는 파라미터 클래스
  class Node
  {
    // 콘솔 출력시 사용되는 텍스트
    public string Text { get; set; }
    // 반복문 횟수
    public int Count { get; set; }
    // Sleep의 시간틱
    public int Tick { get; set; }
  }
  class Program
  {
    private static async Task<int> RunAsync(Node node)
    {
      var task = new Task<int>(() =>
      {
        // 변수
        int sum = 0;
        // 설정된 반복문의 횟수만큼
        for (int i = 0; i <= node.Count; i++)
        {
          // 값을 더하기
          sum += i;
          // 콘솔 출력
          Console.WriteLine(node.Text + " = " + i);
          // 설정된 Sleep 시간틱
          Thread.Sleep(node.Tick);
        }
        // 콘솔 출력
        Console.WriteLine("Completed " + node.Text);
        return sum;
      });
      // Task 실행
      task.Start();
      // task가 종료될 때까지 대기
      await task;
      // task의 결과 리턴
      return task.Result;
    }
    // 실행 함수
    static void Main(string[] args)
    {
      // ThreadPool의 최소 쓰레드는 0, 최대 쓰레드는 2개로 설정
      ThreadPool.SetMinThreads(0, 0);
      ThreadPool.SetMaxThreads(2, 0);
      // 리스트에 Task를 설정한다.
      var list = new List<Task<int>>();

      // 리스트에 Tack 추가
      // ContinueWith 함수를 사용하여 쓰레드의 결과를 받으면 계산을 추가합니다.
      list.Add(RunAsync(new Node { Text = "A", Count = 5, Tick = 1000 }).ContinueWith(x => x.Result * 100));
      list.Add(RunAsync(new Node { Text = "B", Count = 5, Tick = 10 }).ContinueWith(x => x.Result * 100));
      list.Add(RunAsync(new Node { Text = "C", Count = 5, Tick = 500 }).ContinueWith(x => x.Result * 100));
      list.Add(RunAsync(new Node { Text = "D", Count = 5, Tick = 300 }).ContinueWith(x => x.Result * 100));
      list.Add(RunAsync(new Node { Text = "E", Count = 5, Tick = 200 }).ContinueWith(x => x.Result * 100));
      // 쓰레드의 합을 출력
      Console.WriteLine("Sum = " + list.Sum(x => x.Result));
      // 아무 키나 누르면 종료
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}

Task에서 ContinueWith의 함수를 제공하는데, 이는 각 쓰레드가 종료되면 이어서 처리되는 람다식 처리입니다.

상황에 따라서는 다른 Task 쓰레드를 붙일 수도 있고 여러가지 실행을 연결해서 구현할 수 있는 함수입니다.


Task는 .Net framework 4.0부터 추가된 기능이므로 만약 그 이전의 프레임워크라면 ThreadPool을 사용 해야 합니다.

처도 최근까지 ThreadPool를 많이 사용했는데, Task 쓰레드의 사용법을 적응한 후부터는 Task 쓰레드가 너무 편해서, Thread나 ThreadPool은 이제 못 쓰겠네요..

그리고 async, await의 키워드로 인해 가독성 면에서도 너무 좋아져서 개인적으로 병렬 처리를 작성하면 Task를 이용해서 작성하는 것을 추천합니다.


여기까지 C#에서 Task 클래스와 async, await 사용법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.