Thread 04. Task, Async, Await
비동기 프로그래밍
CPU(중앙처리장치)는 정말 많은 작업을 요청받고 처리한다.
데이터를 읽고 쓰고, 네트워크로 데이터를 주고 받고, 화면 픽셀을 계산하는 작업 모두 중앙처리장치의 처리가 필요하다.
이런 중앙처리장치가, 특정 작업의 완료를 기다리는 것은 너무 비효율적이다.
그래서 중앙처리장치가 비효율적으로 낭비되지 않도록 비동기 프로그래밍 기술을 사용하여, 중앙처리장치가 입출력과 같은 낭비될만한 작업을 기다리는 대신 다른 업무를 처리하도록 하고 입출력이 완료되었을 때 기존 작업을 다시 시작하도록 프로그래밍한다.
C# 에서 비동기 프로그래밍
C# 에서 비동기 프로그래밍 Thread, ThreadPool 기반 기술로 시작했지만, Task Parallel Library (TPL) 기반 기술과 async / await 키워드 구현 개념만 이해하면 비동기 로직을 손쉽게 구현할 수 있다.
Task 기반 비동기 프로그래밍
Task 기반 비동기 패턴 (Task-based asynchronous pattern)
Task 등장 배경
ThreadPool 문제점
- ThreadPool의 스레드는 재사용되므로 특정한 이름을 지정할 수 없다.
- 기본적으로 Thread.Join 같은 메서드도 사용할 수 없어 프로세스의 흐름을 통제 할 수 없다.
- ThreadPool 은 백그라운드 스레드로 작업 중에 프로세스가 종료될 위험이 있다.
- 리턴값을 쉽게 돌려 받지 못한다.
- Thread Pool에 남아있는 자원이 없다면 더 이상 스레드를 생성할 수 없다.
Task
동작 방식
- Task 클래스는 내부적으로 ThreadPool 큐 라는 스레드 관리 서비스에 등록되어 실행된다.
- ThreadPool 은 개발자를 대신해 시스템의 코어 개수에 비례하여 스레드 수를 조정하며 처리량을 최대화 하는 방향으로 부하를 분산시킨다.
- 비동기 작업의 상태를 확인할 수도 있다.
- 비동기 작업의 흐름을 제어 할 수 있다.
- 비동기 작업을 시작하거나 취소하거나 종료할 수도 있다. 당연히 예외도 처리 가능하다.
- 리턴값을 돌려받기 쉽다.
예제
- 커피 한 잔을 준비한다.
- 팬을 데운 다음 계란 두 개를 볶는다.
- 베이컨 세 조각을 볶는다.
- 빵 두 조각을 토스트기에 굽는다.
- 토스트에 버터와 잼을 바른다.
- 오렌지 주스 한 잔을 붓는다.
동기적으로 실행의 경우
각각의 아침 식사를 만들기 위한 작업을 하나씩 진행한다.
비동기적으로 실행의 경우
한 사람(또는 스레드)이 첫 번째 작업이 완료되기 전에 다음 작업을 시작하여 비동기적으로 아침 식사를 만들 수 있습니다. 계란을 볶기 위해 팬을 데우기 시작하자마자 베이컨을 볶을 수도 있습니다.
빵을 토스터에 넣어 놓고 오렌지 주스를 준비할 수 있습니다.
동기적 실행 코드
- 실행의 흐름을 블록한다.
- 하나의 작업이 완료될때까지 다른 작업을 진행 할 수 없다.
static void Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("커피가 준비되었습니다.");
Egg eggs = FryEggs(2);
Console.WriteLine("계란 후라이가 준비되었습니다.");
Bacon bacon = FryBacon(3);
Console.WriteLine("베이컨이 준비되었습니다.");
Toast toast = ToastBread(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("토스트가 준비되었습니다.");
Juice oj = PourOJ();
Console.WriteLine("오렌지 주스가 준비되었습니다.");
Console.WriteLine("아침식사 준비 작업이 끝났습니다.!");
}
using System;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class Bacon { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static void Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("커피가 준비되었습니다.");
Egg eggs = FryEggs(2);
Console.WriteLine("계란 후라이가 준비되었습니다.");
Bacon bacon = FryBacon(3);
Console.WriteLine("베이컨이 준비되었습니다.");
Toast toast = ToastBread(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("토스트가 준비되었습니다.");
Juice oj = PourOJ();
Console.WriteLine("오렌지 주스가 준비되었습니다.");
Console.WriteLine("아침식사 준비 작업이 끝났습니다.!");
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static Toast ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
Task.Delay(3000).Wait();
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static Bacon FryBacon(int slices)
{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
Task.Delay(3000).Wait();
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
Task.Delay(3000).Wait();
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static Egg FryEggs(int howMany)
{
Console.WriteLine("Warming the egg pan...");
Task.Delay(3000).Wait();
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
Task.Delay(3000).Wait();
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
비동기적 실행 코드로 변환
- 여러 작업이 상호간에 실행의 흐름을 간섭하지 않는다.
- 하나의 작업이 진행중에 완료될때까지 다른 작업을 진행 할 수 있다.
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("커피가 준비되었습니다.");
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("계란 후라이가 준비되었습니다.");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("베이컨이 준비되었습니다.");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("토스트가 준비되었습니다.");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Juice oj = PourOJ();
Console.WriteLine("오렌지 주스가 준비되었습니다.");
Console.WriteLine("아침식사 준비 작업이 끝났습니다.!");
}
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class Bacon { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("커피가 준비되었습니다.");
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("계란 후라이가 준비되었습니다.");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("베이컨이 준비되었습니다.");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("토스트가 준비되었습니다.");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Juice oj = PourOJ();
Console.WriteLine("오렌지 주스가 준비되었습니다.");
Console.WriteLine("아침식사 준비 작업이 끝났습니다.!");
}
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(3000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static async Task<Bacon> FryBaconAsync(int slices)
{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
await Task.Delay(3000);
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
await Task.Delay(3000);
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
예제
Task.Factory.StartNew( );
- 쓰레드를 생성과 동시에 실행하는 방법
namespace TPLTest
{
using System;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
// Task.Factory를 이용하여 스레드 생성과 시작
Task.Factory.StartNew(new Action<object>(Run), null);
Task.Factory.StartNew(new Action<object>(Run), "1st");
Task.Factory.StartNew(Run, "2nd");
// 아무 키나 누르면 종료
Console.WriteLine("Press Any key...");
Console.ReadLine();
}
static void Run(object data)
{
Console.WriteLine(data == null ? "NULL" : data);
}
}
}
Task t1 = new Task( );
t1.Start();
t1.Wait();
- Task는 ThreadPool을 이용하지만, Thread 처럼 시작과 Join을 사용할 수 있다.
namespace TPLTest
{
using System;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
// Task 생성자에 Run을 지정 Task 객체 생성
Task t1 = new Task(new Action(Run));
// 람다식을 이용 Task객체 생성
Task t2 = new Task(() =>
{
Console.WriteLine("Long query");
Thread.Sleep(1001);
});
// Task 쓰레드 시작
t1.Start();
t2.Start();
// Task가 끝날 때까지 대기
t1.Wait();
t2.Wait();
// 아무 키나 누르면 종료
Console.WriteLine("Press Any key...");
Console.ReadLine();
}
static void Run()
{
Console.WriteLine("Long running method");
Thread.Sleep(1001);
}
}
}
Task<T> 클래스
- 리턴값을 사용하기 쉽다.
- Task<T> 클래스의 T는 리턴 타입
- 리턴값은 Task객체의 Result 속성으로 참조 가능하다.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ThreadTest
{
class Program
{
static void Main(string[] args)
{
// Task<T>를 이용하여 스레드 생성하고
// <T> 타입 반환받는다.
// Task.Factory.StartNew로 시작
Task<int> task = Task.Factory.StartNew<int>(() => StrSize("Hello World"));
// 메인스레드에서 다른 작업 실행
Thread.Sleep(1000);
// Task객체의 Result 속성 Task 실행 결과를 반환 받을 수 있다.
// 스레드가 실행중이면 끝날 때까지 대기함
int result = task.Result;
Console.WriteLine("Result={0}", result);
}
static int StrSize(string data)
{
// 복잡한 계산 가정
string s = data == null ? "" : data.ToString();
// string 길이 반환
return s.Length;
}
}
}
Task의 ThreadPool 갯수 제한
- Task는 기본적으로 ThreadPool을 이용하기에 ThreadPool 처럼 Thread 갯수 제한이 가능하다.
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace ThreadTest
{
// 스레드로 전달 될 파라미터 클래스
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, 2);
// 리스트에 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);
Thread.Sleep(node.Tick);
}
// 콘솔 출력
Console.WriteLine("Completed " + node.Text);
// 결과값 리턴
return sum;
});
// 리스트에 Task 작업 추가, 스레드풀의 제한된 갯수만큼 동작
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.ForEach(x => x.Wait());
// 스레드에서 더해진 합을 출력
//lock를 사용하지 않아도 각 스레드 결과 값을 사용
Console.WriteLine("Sum = " + list.Sum(x => x.Result));
// 아무 키나 누르면 종료
Console.WriteLine("Press Any key...");
Console.ReadLine();
}
}
}
Task는 별도의 스레드를 생성 가능하다.
using System;
using System.Threading;
using System.Threading.Tasks;
public class Program
{
static void MainThread(object? obj)
{
for (int i = 0; i < 5; i++) Console.WriteLine("Create Thread");
}
static void Main(string[] args)
{
// 쓰레드를 최소 1개 최대 3개까지밖에 못빌려주도록 제한
ThreadPool.SetMinThreads(1, 1);
ThreadPool.SetMaxThreads(3, 3);
// 스레드 풀에서 스레드를 3개 가져와 무한 실행
for (int i = 0; i < 3; i++)
{
// Task의 LongRunning 옵션 사용
Task t = new Task(() => { while (true) ; }, TaskCreationOptions.LongRunning);
t.Start();
}
// Task도 기본적으로 ThreadPool에서 돌아가지만
// LongRunning 옵션을 생성해 둔 것은 별도의 스레드를 생성한다.
ThreadPool.QueueUserWorkItem(MainThread);
ThreadPool.QueueUserWorkItem(MainThread);
// 아무 키나 누르면 종료
Console.WriteLine("Press Any key...");
Console.ReadLine();
}
}
async & await
컴파일러는 async 키워드(한정자)를 만나면 블록하지 않고 바로 다음 작업이 진행되도록 하는 비동기 코드를 생성다.
async한정자는 메서드, 이벤트, 태스크, 람다식에서 사용될 수 있으므로, 이 한정자를 사용하는 것만으로 비동기 코드를 만들수 있다.
async가 사용된 메서드 내부에서는 await 연산자가 사용되는데
메서드가 실행된 이후 await 연산자가 실행되면 제어권을 호출자에게 돌려준다.
await 다음 실행문부터는 호출자와 병렬적인 비동기 동작한다.
예제1
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
printNumber();
for (int i = 0; i <= int.MaxValue; i++)
{
Console.WriteLine("실행" + i.ToString());
Thread.Sleep(500);
}
}
async static void printNumber()
{
await Task.Run(() => {
for (int i = 0; i <= int.MaxValue; i++)
{
Console.WriteLine("비동기실행" + i.ToString());
Task.Delay(1000).Wait();
}
});
}
}
예제2
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
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);
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, 2);
// 리스트에 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 }));
// 스레드에서 더해진 합을 출력
//lock를 사용하지 않아도 각 스레드 결과 값을 사용
Console.WriteLine("Sum = " + list.Sum(x => x.Result));
// 아무 키나 누르면 종료
Console.WriteLine("Press Any key...");
Console.ReadLine();
}
}
}