- Thread 관련 내용
- 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");
}
}
}
- 위 코드의 실행 과정은 아래와 같다
- Main Method 진입
- Main Thread에서 TaskTest Method 호출
- 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를 사용
- Main Thread에서 "Main Thread is NOT Blocked"를 출력
- 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");
}
}
}
- 위 코드의 진행 순서는 아래와 같다
- "Before TaskTest" 10번 출력
- Task.Delay(5000) : 5초간의 Delay 후 "TaskTest Done" 출력
- 'await' keyword로 인해 이 Task 종료 전까지 다른 동작은 수행되지 않음
- "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
https://en.wikipedia.org/wiki/Thread_pool
https://hochoon-dev.tistory.com/entry/Thread-Pool%EC%9D%B4%EB%9E%80
https://velog.io/@dodozee/OSJava-%EC%8A%A4%EB%A0%88%EB%93%9C-%ED%92%80%EC%9D%B4%EB%9E%80Thread-pool
https://luvris2.tistory.com/559
https://yangbengdictionary.tistory.com/14
https://velog.io/@fastfox/Thread.sleep%EA%B3%BC-Task.delay%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90
https://www.csharpstudy.com/WinForms/WinForms-backgroundworker.aspx
'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 |