본문 바로가기

Study_C#

[C#] 비동기 관련

728x90

- Thread 관련 내용

 

Thread란

- ProcessComputer에서 연속적으로 실행되고 있는 ProgramOS로부터 Memory를 할당받아 실행 중인 것Scheduling의 대상이 되는 Task와 거의 같은 의미로 쓰임여러 개의 Processor를 사용하는 것을 Multi Processing,같

youngseong.tistory.com

 

  • C#에서의 비동기 프로그래밍은 별도의 라이브러리 없이 내장된 비동기 모델을 통해 구현 가능

 

- 관련 Keyword

1) Task

  • 비동기 작업 Wrapper
  • 비동기 작업 Modeling시에 사용되는 Keyword

 

2) async

  • 해당 Method를 'await' Keyword를 사용할 수 있는 비동기 Method로 변환해주는 Keyword
    • async Method는 반드시 void / Task / Task<T>를 Return해야 한다
    • 단 void를 Return 시 비동기 Method를 호출하는 쪽에서 비동기 제어가 불가능하므로 보통 Task나 Task<T>를 사용한다
    • UI 버튼을 클릭하면 일어나는 Event들을 비동기로 처리할 때 void를 사용한다
  • 이 키워드를 통해 바로 비동기 방식으로 프로그램을 수행하는 것이 아닌, 일종의 보조 역할

 

3) await

  • 비동기 작업의 흐름을 제어하는 Keyword. 비동기 Method 내부에서만 사용 가능
    • 비동기 작업이 실행될 수 있는 곳이 await
    • 흐름을 정할 수 있다는 것은 작업의 순서를 정할 수 있다는 뜻

 

var t = Task.Run(async delegate
            {
                await Task.Delay(1000);
                return 42;
            });
  • 비동기 Task를 구성하고 시작하는 코드
  • Task.Run : delegate를 실행할 새 Task 생성 및 시작
    • 특정 작업이 Thread Pool 상에서 실행되도록 대기열에 넣고 그 작업을 의미하는 'Work'를 Return하는 Method
    • Task를 Background Thread에 놓아 Main Thread가 계속 작업을 수행할 수 있도록 한다
    • Task : 비동기 작업 Wrapper
    • 즉 Delay(1000) 작업을 Task의 형태로 Wrapping한다
    • Delay는 Thread의 실행을 중지하지 않는다 (Thread.Sleep(x)이 x만큼 Thread를 중지함)
  • delegate : Method를 매개 인자로 사용할 수 있는 기능
  • async : 위의 delegate가 비동기 동작을 포함하며 'await' Keyword를 사용할 수 있음을 의미
  • await Task.Delay(1000) : 1초 Delay 후에 종료되는 Task 생성
    • await : Thread를 막지 않고 위 Task의 완료를 비동기적으로 기다림
  • return 42 : Delay 후 delegate가 반환하는 값
    • 위 return 문이 async 내부에 있으므로 Return Type은 'Task<int>' 이다

 

 

  • 위처럼 Task를 await할 시, 현재 Method의 실행을 await된 Task가 끝날 때 까지 잠시 멈춘다
    • Calling thread를 block하진 않는다
    • 대신 Control이 Caller에게 넘어가고, method의 실행 상태는 유지된다
  • await된 Task 완료 시, 현재 method의 멈춘 지점에서부터 실행을 다시 시작한다
  • Debug Mode에서 await 구문을 만날 경우 debugger는 Task가 종료될 때 까지 현재 method를 종료한다
  •  

 

 

* Thread.Sleep()과 Task.Delay()의 차이

  • Thread.sleep() : 지정한 ms동안 스레드를 차단하는 동기 함수
  • Task.delay() : 현재 스레드를 차단하지 않고 논리적 지연을 원할 때 사용하는 비동기 함수

 

  • Task.Run을 통해 delegate를 실행할 Task 생성 및 실행
  • delegate는 async 표시가 되어 있어 내부에서 'await' keyword를 사용할 수 있다
    • delegate 내부에선 await Task.Delay(1000) 이 비동기적으로 1초를 기다린다
  • 1초의 Delay 후, delegate는 '42'를 Return한다
    • 이 때 delegte는 비동기이므로, return 값은 Task<int> 형으로 변환된다
  • 변수 't'에는 Task.Run을 통해 생성된 Task가 할당됨
    • 't'의 type은 Task<int>이며, 이는 Interger Type의 결과를 가짐을 의미

 

 


-  C# Console / Web Apps 실행 예제 (Windows Form 같이 GUI를 다루는 App은 다르게 동작)

  • 동기 코드 예제
using System;
using System.Threading.Tasks;
 
namespace AsyncTest
{
    class Program
    {
        public static void Main(string[] args)
        {
            TaskTest();
            System.Console.WriteLine("Main Done");
        }
        private static void TaskTest()
        {
            Task.Delay(5000);
            System.Console.WriteLine("TaskTest Done");
        }
    }
}

 

  • 위 코드의 경우, Task.Delay(5000)의 반환 값인 5초를 기다리는 Task를 await하지 않음
    • Task의 Status는 WaitingForActivation(대기) -> 5초 후 RanToCompletion(완료) 됨
    • 하지만 프로그램은 이 5초를 기다리는 작업이 완료되길 기다리지 않음
    • 이로 인해 "Main Done"과 함께 "TaskTest Done"이 동시에 출력됨
  • 따라서 'await' Keyword 없이 Task와 같은 awaitable을 사용하면 작업의 종료 시점이 언젠지 알 수 없음
    • 이로 인해 작업을 통제할 수 없게 됨

 

  • 비동기 작업 통제를 위해 TaskTest method에 'async' Keyword, Task.Delay(5000) 앞에 'await' Keyword 추가
using System;
using System.Threading.Tasks;
 
namespace AsyncTest
{
    class Program
    {
        public static void Main(string[] args)
        {
            TaskTest();
            System.Console.WriteLine("Main Thread is NOT Blocked");
            Console.ReadLine();
        }
        private static async void TaskTest()
        {
            await Task.Delay(5000);
            System.Console.WriteLine("TaskTest Done");
        }
    }
}
  • 위 코드의 실행 과정은 아래와 같다
  1. Main Method 진입
  2. Main Thread에서 TaskTest Method 호출
  3. TaskTest Method 내의 await Task.Delay(5000) 실행
    • Thread Pool의 Thread가 Task.Delay(5000)을 실행하고
    • await에 의해 작업의 흐름이 TaskTest를 호출한 Main Thread로 넘어감
    • Task.Delay는 새 Thread를 생성하지 않고, 내부적으로 Thread Pool을 사용하는 Timer를 사용함
    • Main Thread를 5초간 Block 하지 않음 (Thread.Sleep(5000)의 경우는 Thread를 5초간 중지시킴)
    • 5초 후 완료되는 작업을 Task 형태로 Wrapping 함
    • Task.Delay는 새로운 Thread를 생성하지 않고 내부적으로 Thread Pool을 사용하는 Timer를 사용 
  4. Main Thread에서 "Main Thread is NOT Blocked"를 출력
  5. 5초 후에 Task.Delay(5000) 작업 종료 후Thread Pool에 있는 잉여 Thread가 "TaskTest Done"을 출력

 

  • 동기로 실행되는 코드 사이에  await Keyword를 통해 작업 순서 지정
    • Main method에 'async' Keyword 추가 후 return type을 Task로 변경
    • TaskTest method의 return type Task로 변경
    • Task t = TaskTest()를 통해 TaskTest method로부터 t를 반환받음
    • await t : t(awaitable)이 끝날 때 까지 기다림
using System;
using System.Threading.Tasks;
 
namespace AsyncTest
{
    class Program
    {
        public static async Task Main(string[] args)
        {
            Task t = TaskTest();
            
            for(int i = 0; i < 10; i++)
            {
                System.Console.WriteLine("Before TaskTest");
            }
 
            await t;
 
            for (int i = 0; i < 10; i++)
            {
                System.Console.WriteLine("After TaskTest");
            }
 
            Console.ReadLine();
        }
 
        private static async Task TaskTest()
        {
            await Task.Delay(5000);
            System.Console.WriteLine("TaskTest Done");
        }
    }
}
  • 위 코드의 진행 순서는 아래와 같다
  1. "Before TaskTest" 10번 출력
  2. Task.Delay(5000) : 5초간의 Delay 후 "TaskTest Done" 출력
    • 'await' keyword로 인해 이 Task 종료 전까지 다른 동작은 수행되지 않음
  3. "After TaskTest" 10번 출력
  • 첫번째 예제의 경우 TaskTest method가 async void : Main method가 이 TaskTest method를 await하지 않음
    • 즉 Main method에서는 await할 내용이 존재하지 않음
  • 두번째 예제의 경우 TaskTest method가 awaitable인 Task를 main thread에 반환함
  • 또한 이를 await t를 통해 호출하므로, Main method가 TaskTest method를 await 함
    • 즉 TaskTest method가 Main method에 Task를 반환하고, Main method에서 이 Task를 await함
  • 즉 호출자 method로부터 await되기 위해선 async Task를 반환해야 함
    • 또한 호출자 method는 'async' Keyword를 이름에 포함하여 await를 사용 가능한 형태이어야 함
  • 위처럼 'await' keyword는 비동기 작업(async Task)의 흐름(순서)를 제어할 수 있다

 

 

- 만약 Task.Delay(5000)이 단순히 5초를 기다리는 것이 아닌 외부와의 통신 처럼 매우 오래 걸리는 작업이고, 그 작업이 종료된 후 어떤 값을 반환할 수 있다 가정

private static async Task<int> TaskTest()
{
    await Task.Delay(5000); // UID를 수신받아 오는 부분이라 가정
    System.Console.WriteLine("TaskTest Done");
 
     int UID = 100	    // 수신받은 Data를 변환한 값
 
    return UID;
}
  • Ex) Server로부터 어떤 User의 ID를 받아와 반환한다 가정
  • 이 반환된 UID를 Main method에서 받음

 

public static async Task Main(string[] args)
{
    Task<int> t = TaskTest();
            
    for(int i = 0; i < 10; i++)
    {
        System.Console.WriteLine("Before TaskTest");
    }
 
    int UID = await t;
 
    Console.WriteLine($"UserID : {UID}");
 
    Console.ReadLine();
}

 

  • Task t = TaskTest() -> Task<int> t = TaskTest()로 변경 (Generic 추가)
  • 위처럼 반환 값이 있는 경우 await를 통해서 반환 값을 추출할 수 있다

 

- 비동기 코드 예제

static async Task Main(string[] args)
{
    Task.Delay(5000).Wait();
 
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
 
    var eggsTask = FryEggsAsync(2);
    var baconTask = FryBaconAsync(3);
    var toastTask = MakeToastWithButterAndJamAsync(2);
 
    var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
    while (breakfastTasks.Count > 0)
    {
        Task finishedTask = await Task.WhenAny(breakfastTasks);
        if (finishedTask == eggsTask)
        {
        	Console.WriteLine("eggs are ready");
        }
        else if (finishedTask == baconTask)
        {
        	Console.WriteLine("bacon is ready");
        }
        else if (finishedTask == toastTask)
        {
        	Console.WriteLine("toast is ready");
        }
        breakfastTasks.Remove(finishedTask);
    }
 
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");
    Console.WriteLine("Breakfast is ready!");
}
 
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
    var toast = await ToastBreadAsync(number);
    ApplyButter(toast);
    ApplyJam(toast);
 
    return toast;
}

 

  • List에 Task를 저장하고 가장 빠르게 조리된 음식을 출력하며 이후에 List에서 Task를 삭제
  • 임의의 Task를 고정적으로 기다릴 필요 없이 완료된 시간에 따라 Task의 처리가 이뤄짐

 

 

 

- BackgroundWorker

  • Background Thread에서 동작을 수행할 수 있도록 하는 .NET Framework의 Class
    • Main UI Thread와 별개의 Thread에서 병렬적으로 동작
    • 즉 별도의 Thread에 어떤 일을 시키기 위해 사용하는, 비동기 동작을 지원하는 C#의 사전지원 Class
  • 이러한 작업 분리를 통해 Main UI Thread의 부하를 덜어주어 원활한 UI의 동작을 가능하게 함
  • BackgroundWorker로 생성된 Object는 'DoWork' event hanlder를 통해 실제 작업할 내용을 지정함
    • ProgressChanged Event를 통해 진척 사항 전달
    • RunWorkerCompleted Event를 통해 완료 후 실행될 작업 지정
    • DoWork는 Worker Thread에서, 나머지 두 Event들은 UI Thread에서 실행
  • BackgroundWorker Class Object는 Thread Class와 같이 Thread를 직접 생성하는 것이 아닌 Thread Pool로부터 가져온 Thread를 사용

 

- 관련 method

  • RunWorkerAsync() : 호출 시 새로운 background thread에서 'DoWork' Event handler 시작
  • ProgressChanged() : 진척 사항 전달
  • RunWorkerCompleted() : 지정한 작업 완료 후 실행될 작업 지정

 

public partial class Form1 : Form
{
   private BackgroundWorker worker;

   public Form1()
   {
      InitializeComponent();
      worker = new BackgroundWorker();
      worker.WorkerReportsProgress = true;
      worker.WorkerSupportsCancellation = true;
      worker.DoWork += new DoWorkEventHandler(worker_DoWork);         
      worker.ProgressChanged += new ProgressChangedEventHandler(worker_ProgressChanged);
      worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
   }

   // Worker Thread가 실제 하는 일
   void worker_DoWork(object sender, DoWorkEventArgs e)
   {
      string srcDir = @"C:\Temp\_Src";
      string destDir = @"C:\Temp\_Dest";

      DirectoryInfo di = new DirectoryInfo(srcDir);
      FileInfo[] fileInfos = di.GetFiles();
      int totalFiles = fileInfos.Length;
      int counter = 0;
      int pct = 0;
      foreach (var fi in fileInfos)
      {
         string destFile = Path.Combine(destDir, fi.Name);
         File.Copy(fi.FullName, destFile);            
            
         pct = ((++counter * 100) / totalFiles);            
         worker.ReportProgress(pct);
      }         
   }

   // Progress 리포트 - UI Thread
   void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
   {
      this.progressBar1.Value = e.ProgressPercentage;            
   }

   // 작업 완료 - UI Thread
   void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
   {
      // 에러가 있는지 체크
      if (e.Error != null)
      {
         lblMsg.Text = e.Error.Message;
         MessageBox.Show(e.Error.Message, "Error");          
         return;
      }

      lblMsg.Text = "성공적으로 완료되었습니다";
   }      

   private void btnRun_Click(object sender, EventArgs e)
   {
      // 비동기(Async)로 실행 
      worker.RunWorkerAsync();
   }
}
  • BackgroundWorker를 통해 파일 복사를 비동기적으로 수행하는 코드
  • 파일이 복사되는 진행 사항이 ProgressBar Control을 통해 보여짐
  • 많은 파일 복사가 일어날 경우, 비동기 처리를 하지 않으면 UI Thread가 너무 바빠져 UI가 먹통인 경우 발생 가

 

 

 

 

 

 

 

 

 

 

 

 


참고 자료 : 

https://kangworld.tistory.com/25

 

[C#] async await 예제 코드 #2 (+ 동기 비동기의 개념)

async await 두 번째 편이자 마지막 편! 빵! 끗! 인트로 이틀간 밤을 새우며 stackoverflow와 저명한 C# 개발자의 개인 홈페이지에서 글을 읽으며 async await 개념을 정리했다. 아직도 궁금한 부분이 많고

kangworld.tistory.com

 

https://en.wikipedia.org/wiki/Thread_pool

 

Thread pool - Wikipedia

From Wikipedia, the free encyclopedia Software design pattern A sample thread pool (green boxes) with waiting tasks (blue) and completed tasks (yellow) In computer programming, a thread pool is a software design pattern for achieving concurrency of executi

en.wikipedia.org

 

https://hochoon-dev.tistory.com/entry/Thread-Pool%EC%9D%B4%EB%9E%80

 

Thread Pool이란?

Thread Pool 회사 제품의 코드를 보면서 긴가민가했던 개념들을 정리해볼라고 적는 포스팅 Process VS Thread 맨날 프로세스가 더 큰거, 쓰레드는 그 프로세스 안에서 돌아가는 단위로서 프로세스의 자

hochoon-dev.tistory.com

 

https://velog.io/@dodozee/OSJava-%EC%8A%A4%EB%A0%88%EB%93%9C-%ED%92%80%EC%9D%B4%EB%9E%80Thread-pool

 

[OS,Java] 스레드 풀이란?(Thread pool)

스레드의 생성과 소멸이 자주되면 시스템에 많은 부담을 준다. 스레드가 하나의 일에 한번만 수행하고 소멸된다면 매우 비효율적인 일이 아닐수가 없다!.오늘은 스레드를 어떻게 효율적으로 사

velog.io

 

https://luvris2.tistory.com/559

 

C# - 비동기 프로그래밍(async, await) - 개념, 사용 방법, 비동기식 병렬 처리

동기식 vs 비동기식 동기식 비동기식 작업 처리 순차적 처리, 하나의 작업이 끝나야 다음 작업 시작 동시 처리, 한 작업이 완료되기 전에 다음 작업 수행 장점 순차적으로 실행되기 때문에 간단

luvris2.tistory.com

 

https://yangbengdictionary.tistory.com/14

 

[C#] BackgroundWorker

BackgroundWorker 특징 BackgroundWorker클래스는 별도의 스레드에게 어떤 일을 시키기 위해 사용하는 클래스이다. Thread이지만, 별도의 작업없이도 UI를 제어할 수 있다.이벤트 DoWork이벤트 핸들러를 통해

yangbengdictionary.tistory.com

 

https://velog.io/@fastfox/Thread.sleep%EA%B3%BC-Task.delay%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90

 

Thread.sleep()과 Task.delay()의 차이점

Thread.sleep() : 동기함수이며, 지정한 ms동안 스레드를 차단하는 함수이다.Task.delay() : 비동기 함수이며, 현재 스레드를 차단하지 않고 논리적 지연을 원할 때 사용하는 함수이다.위에서 설명한 두

velog.io

 

 

https://www.csharpstudy.com/WinForms/WinForms-backgroundworker.aspx

 

BackgroundWorker - C# 프로그래밍 배우기 (Learn C# Programming)

BackgroundWorker 클래스 BackgroundWorker클래스는 별도의 쓰레드에게 어떤 일을 시키기 위해 사용하는 클래스이다. 흔히 백그라운 쓰레드 혹은 워커 쓰레스라 불리우는 별도의 쓰레드에서는 UI Thread와

www.csharpstudy.com

 

 

 

 

'Study_C#' 카테고리의 다른 글

[C#] Invoke, InvokeRequired, Delegate  (0) 2024.07.30
[C#] Using의 2가지 사용법  (0) 2024.07.30
[C#] 자료형 정리  (0) 2024.07.06
Doridori C# 강의 정리 2. Data Type과 Overflow  (0) 2024.05.22
Doridori C# 강의 정리 1. String  (4) 2024.05.20