■ 동기 처리란?
앞 처리의 결과가 반환돼야 다음 처리로 넘어가는 것
ex) 요리하기
■ 비동기 처리란?
앞 처리의 결과를 기다리지 않고 다음 처리로 넘어가는 것
ex) 요리하면서 욕실에 물 받기
■ 비동기 처리의 종류
Thread, Task, async, await
■ 옛 방식의 비동기 처리 「Thread」
아래는 메인 스레드에서는 HeavyMethod2를, 다른 별도의 스레드에서 HeavyMethod1를 비동기로 실행하는 소스이다.
Program
{
static void Main(String[] args)
{
Thread thread = new Thread(new ThreadStart(() =>
{
HeavyMethod1();
}));
thread.Start();
HeavyMethod2();
Console.ReadLine();
}
static void HeavyMethod1()
{
Console.WriteLine("매우 무거운 처리1 시작");
Thread.Sleep(5000);
Console.WriteLine("매우 무거운 처리1 종료");
}
static void HeavyMethod2()
{
Console.WriteLine("매우 무거운 처리2 시작");
Thread.Sleep(3000);
Console.WriteLine("매우 무거운 처리2 종료");
}
}
결과
매우 무거운 처리2 시작 매우 무거운 처리1 시작 매우 무거운 처리2 종료 매우 무거운 처리1 종료 |
■ 비동기 처리 「Task」
Task는 Thread에서는 할 수 없는, 다음 세 가지를 실현할 수 있다.
- 비동기로 실시한 처리의 상태(실행중, 완료, 취소, 에러)를 알 수 있다.
- 예외를 보충(처리 할 수 있다는 말인가?)할 수 있다.
- 비동기 처리의 실행순서를 제어할 수 있다.
Program
{
static void Main(string[] args)
{
// Task를 생성한다.
// Task.Run메소드는 Task를 생성하는 Factory 클래스로, Task형의 객체를 반환한다.
// 인수에는 delegate를 전달한다.
// 단순히 HeavyMethod1을 실행하고 싶은 경우, 인수가 없는 delegate를 전달한다.
Task task = Task.Run(() => {
HeavyMethod1();
});
HeavyMethod2();
Console.ReadLine();
}
static void HeavyMethod1()
{
Console.WriteLine("매우 무거운 처리1 시작");
Thread.Sleep(5000);
Console.WriteLine("매우 무거운 처리1 종료");
}
static void HeavyMethod2()
{
Console.WriteLine("매우 무거운 처리2 시작");
Thread.Sleep(3000);
Console.WriteLine("매우 무거운 처리2 종료");
}
}
결과
매우 무거운 처리2 시작 매우 무거운 처리1 시작 매우 무거운 처리2 종료 매우 무거운 처리1 종료 |
■ Thread와 Task 비교 ~1~ 「결과값을 취득하고 싶을 때」
・Thread의 경우
Program
{
static void Main(string[] args)
{
// 메인 스레드에서 접근할 수 있는 변수를 정의하고 이 곳에 결과를 저장한다.
string result = "";
Thread thread = new Thread(new ThreadStart(() =>
{
// 메인스레드와 다른 스레드로 HeavyMethod를 실행하여,
// 결과(이러쿵저러쿵)을 result에 저장한다.
result = HeavyMethod();
}));
// 스레드를 시작한다.
thread.Start();
// Join 메소드를 사용하면 스레드가 종료할 때까지 다음 처리를 실행하지 않는다.
thread.Join();
// 결과를 콘솔에 표시한다.
Console.WriteLine(result);
Console.ReadLine();
}
static string HeavyMethod()
{
Console.WriteLine("매우 무거운 처리 시작");
Thrad.Sleep(5000);
Console.WriteLine("매우 무거운 처리 종료");
return "이러쿵저러쿵";
}
}
결과
매우 무거운 처리 시작 매우 무거운 처리 종료 이러쿵저러쿵 |
・Task의 경우
Program
{
static void Main(string[] args)
{
// 결과를 취득하는 경우 Task를 Generic 타입으로 정의하여 그 Generic에 반환값의 타입을 지정한다.
Task<string> task = Task.Run(() =>
{
return HeavyMethod();
});
// 결과를 취득한다.
// Result의 속성을 사용하면 Task에서 지정한 HeavyMethod가 종료할 때까지 대기하며,
// 결과를 result 변수에 대입해준다.
string result = task.Result;
// 결과를 콘솔에 표시한다.
Console.WriteLine(result);
Console.ReadLine();
}
static string HeavyMethod()
{
Console.WriteLine("매우 무거운 처리 시작");
Thrad.Sleep(5000);
Console.WriteLine("매우 무거운 처리 종료");
return "이러쿵저러쿵";
}
}
결과
매우 무거운 처리 시작 매우 무거운 처리 종료 이러쿵저러쿵 |
Task의 Result 속성을 사용하면, Thread에 비해 직관적이고 간단하게 결과값을 얻을 수 있다.
Result 속성의 사용으로 Thread에서 수행하던 다음 2가지 처리를 하지 않아도 된다.
- 메인스레드에 result 변수를 미리 정의하는 것
- 스레드가 종료할 때까지 Join 메소드로 대기하는 것
→ Task는 개발자가 스레드를 의식하지 않고 동기 처리같은 코드로 비동기 처리를 실현하게 해준다.
■ Thread와 Task 비교 ~2~ 「예외를 보충하고 싶을 때」
・Thread의 경우
Program
{
static void Main(string[] args)
{
try {
Thread thread = new Thread(new ThreadStart(() =>
{
throw new Exception("받아줘 >_<");
HeavyMethod();
}));
thread.Start();
} catch (Exception e) {
Console.WriteLine("받아줘~ Catch!!")
}
Console.WriteLine("종료");
Console.ReadLine();
}
static void HeavyMethod()
{
Console.WriteLine("매우 무거운 처리 시작");
Thread.Sleep(5000);
Console.WriteLine("매우 무거운 처리 종료");
}
}
결과
종료 |
스레드 안의 예외를 처리하지 않는다.
・Task의 경우
Program
{
static void Main(string[] args)
{
Task task = Task.Factory.StartNew(() => {
throw new Exception("받아줘 >_<");
HeavyMethod();
});
try {
// 예외를 캐치하는 경우, Wait 메소드를 실시하는 부분을 try...catch로 감싼다.
// 예외가 발생하면 AggregateException에 Wrap되어 Throw된다.
task.Wait();
} catch (Exception e) {
Console.WriteLine("받아줘~ Catch!!")
}
Console.WriteLine("종료");
Console.ReadLine();
}
static void HeavyMethod()
{
Console.WriteLine("매우 무거운 처리 시작");
Thread.Sleep(5000);
Console.WriteLine("매우 무거운 처리 종료");
}
}
결과
받아줘~ Catch!! 종료 |
스레드 안에서 발생한 예외를 메인스레드가 제대로 캐치했다.
여기서도 마찬가지로 스레드를 의식하지 않고 동기적인 코드로 예외를 캐치했다.
■ Thread와 Task 비교 ~3~ 「다수 스레드의 종료를 기다리고 싶을 때」
케이스 : 정수 1을 반환하는 HeavyMethod1과 정수 2를 반환하는 HeavyMethod2를 병행 실행하여
그 결과를 더해서 콘솔에 3을 출력
・Task의 경우
Program
{
static void Main(string[] args)
{
// 정수 1을 반환하는 HeavyMethod1의 Task를 생성한다.
Task<int> task1 = Task.Run(() => {
return HeavyMethod1();
});
// 정수 2를 반환하는 HeavyMethod2의 Task를 생성한다.
Task<int> task2 = Task.Run(() => {
return HeavyMethod2();
});
// WhenAll의 인수로 생성한 Task를 지정하면 HeavyMethod1, HeavyMethod2가 종료할 때까지
// 다음 코드를 실행하지 않는다. 즉, 대기한다.
Task.WhenAll(task1, task2);
Console.WriteLine(task1.Result + task2.Result);
Console.ReadLine();
}
static int HeavyMethod1()
{
Console.WriteLine("매우 무거운 처리1 시작");
Thread.Sleep(5000);
Console.WriteLine("매우 무거운 처리1 시작");
return 1;
}
static int HeavyMethod2()
{
Console.WriteLine("매우 무거운 처리2 시작");
Thread.Sleep(3000);
Console.WriteLine("매우 무거운 처리2 시작");
return 2;
}
}
결과
매우 무거운 처리2 시작 매우 무거운 처리1 시작 매우 무거운 처리2 종료 매우 무거운 처리1 종료 3 |
■ 정리
Task를 사용하면 Thread로 하던 처리를 매우 심플하게 작성할 수 있다.
출처 : https://tech-lab.sios.jp/archives/15637