Async, Await은 5.0부터 비동기를 쉽게 제공하기 위해 등장했으며 Thread, Task를 공부하는 목적이 Async, Await를 사용해 비동기 프로그래밍을 하는 것 같다. (예를 들어서 Task는 그 자체로 홀로 사용되기 보다는 Async, Await와 함께 사용된다)
그래서 Async, Await를 사용하기 전에 Thread, ThreadPool, Task를 짚고 넘어가자!
+) Task는 Func, Action과 같은 델리게이트를 이해하고 있어야 응용 가능하다 (당연히 람다로도 실행 가능)
C# 4.0 버전에서 나온 Task 클래스는 C# 1.0 부터 있었던 스레드, 스레드풀의 단점을 개선한 클래스이다. 그래서 스레드를 이해하고 있다면 Task도 이해하기 쉽고, 스레드풀을 사용할 일이 있으면 Task를 사용하는 것이 나을 수 있다.
특징 1. Task는 백그라운드 스레드이며 스레드풀을 이용한다.
using System.Threading.Tasks;
...
public class TestTask : MonoBehaviour
{
void Start()
{
Tast task = new Task(BackgroundTask);
task.Start();
}
void BackgroundTask()
{
bool isBackground = Thread.CurrentThread.IsBackground;
bool isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread;
}
}
특징 2. 스레드와 비슷한 기능들이 제공된다. 내부적으로도 이미 스레드풀을 사용하고 있음.
그 예로 Task.Wait() 의 경우 Thread.Join() 처럼 특정 task가 종료될 때까지 현재 task를 블록시킨다.
public class TestTask : MonoBehaviour
{
void Start()
{
ThreadPool.QueueUserWorkItem((obj)=>
{
Debug.Log("스레드풀 실행");
});
Task task = new Task(() =>
{
Debug.Log("Task 실행");
});
task.Start();
task.Wait(); // Thread.Join 메서드와 비슷한 기능
}
}
특징 3. Task가 완료된 후 수행할 작업을 지정할 수 있다.
스레드의 경우 스레드 작업이 끝난 후 이어질 작업을 지정하는 기능이 기존 C# (닷넷 프레임워크 차원) 에서 지원해주지 않는데, Task가 나오면서부터 가능해졌다.
ContinueWith() 로 가능하다.
public class TestTask : MonoBehaviour
{
void Start()
{
Task task = new Task(SleepAction);
// Task가 완료된 후 수행할 작업 지정
task.ContinueWith((obj) =>
{
Debug.Log("Task 완료");
});
task.Start();
}
void SleepAction()
{
Thread.Sleep(3000);
}
}
그리고 아래 특징4의 예시 코드에 등장하는 WhenAll()
또한, 매개변수로 들어오는 Task(들)이 모두 완료될 때까지 대기 후 모든 Task가 완료되면 새로운 Task를 반환한다.
특징 4. Run() 메소드로 Task의 생성과 시작을 한번에 할 수 있다. Start()를 따로 해주지 않아도 바로 시작할 수 있다.
+) Task.Factory.StartNew() 메소드는 Run() 메소드보다 더 많은 선택 옵션이 있다. (취소 기능 등)
참고로 Run() 메소드 정의부터 살펴보면 아래와 같다.
간단한 작업 실행에 주로 사용된다.
그래서 더 복잡한 옵션이 필요하다면 StartNew 가 추천됨.
(CancellationToken은 작업 취소 기능을 제공하는 옵션이다)
...
public static Task Run(Func<Task> function);
public static Task Run(Action action);
public static Task Run(Action action, CancellationToken cancellationToken);
...
사용 예시는 아래와 같다.
public class TestTask : MonoBehaviour
{
private Task task1, task2, task3;
private Stopwatch stopwatch;
void Start()
{
stopwatch = new Stopwatch();
stopwatch.Start();
task1 = Task.Run(Task1Function);
task2 = Task.Run(() =>
{
Thread.Sleep(2000);
});
task3 = Task.WhenAll(task1, task2);
}
private void Update()
{
if(task3.IsCompleted)
{
stopwatch.Stop();
Debug.Log("경과 시간 : "+stopwatch.Elapsed.TotalSeconds+"초");
}
}
void Task1Function()
{
Thread.Sleep(3000);
}
}
특징 5. 기존 스레드, 스레프풀은 결과값을 확인하고 취합할 때 어렵다는 단점 등이 있는데, Task는 결과값 반환으로 가능성이 많아졌다.
public class TestTask : MonoBehaviour
{
void Start()
{
Task<int> task = Task.Run(Test);
task.Wait();
Debug.Log($"실행결과: {task.Result}");
}
int Test()
{
int result = 0;
for(int i=0; i<10; i++)
{
Thread.Sleep(100);
result = i+1;
}
return result;
}
}
Async Await 를 본격적으로 알아보기 전에,
이 개념이 나온 배경을 알아보자!
메인 스레드를 대기/차단(block)시키지 않고 별도의 새로운 스레드에서 다른 작업을 처리하도록 할 수 있다.
C# 1.0 에서 비동기 실행 방법은 BeginInvoke와 EndInvoke메소드로 간단히 해볼 수 있다.
BeginInvoke를 사용하는 예를 보자. 델리게이트를 실행하는 BeginInvoke의 콜백 메소드의 매개변수이자 BeginInvoke의 반환 형식으로는 IAsyncResult이 있다.
그리고 IAsyncResult에는 AsyncState라는 오브젝트 타입이 있다. 이는 BeginInvoke의 두번째 매개변수가 될 수 있다.
public class AsyncTest : MonoBehaviour
{
void Start()
{
Action action = LongRunningOperation;
IAsyncResult result = action.BeginInvoke(new AsyncCallback(EndOperationCallback), null);
Debug.Log("메인스레드 진행 중");
}
void LongRunningOperation()
{
Thread.Sleep(3000);
Debug.Log("오래 걸리는 작업 완료");
}
void EndOperationCallback(IAsyncResult asyncResult)
{
Debug.Log("콜백 실행");
}
}
파일 IO의 동기, 비동기 예시 코드를 캡쳐했다. 왼쪽이 동기, 오른쪽이 비동기이다.
비동기가 동기보다도 상당히 코드의 흐름이 복잡해보임을 알 수 있다. C# 1.0에서는 이런 식으로밖에 할 수 없었다.
이 복잡함을 개선하기 위해 나온 것이 바로 Async Await 이다.
왼쪽이 동기, 오른쪽이 Async Await이다. 훨씬 간편해보인다!
사용 방법을 보기 위해 샘플 코드를 보자.
...
using System.Threading.Tasks;
public class AsyncTest : MonoBehaviour
{
// 비동기 실행할 메서드 앞에 async 넣기
async void Start()
{
Debug.Log("메인 스레드 시작");
// 비동기로 실제로 실행할 부분 앞에 await 넣기
// await 키워드를 만나면 블록된다
// 즉 AsyncSum의 모든 부분이 끝날 때까지 블록된다
// 메인스레드를 차단하지는 않지만, 코드 진행X
await AsyncSum(10, 20);
Debug.Log("메인 스레드 진행 중");
}
async Task AsyncSum(int num1, int num2)
{
// Task.Delay()는 Thread.Sleep()의 비동기 버전으로 메인스레드를 차단하지 않음
await Task.Delay(3000); // 3초 지연
int num3 = num1+num2;
Debug.Log("오래 걸리는 작업 완료");
}
}
실행 결과는 1.0 버전과 좀 다르다. (전혀 비동기스럽지 않아서 당황했다)
메인 스레드 시작
오래 걸리는 작업 완료
메인 스레드 진행 중
여기서 알 수 있는 특징이다.
async await를 사용하면
- 비동기로 호출되더라도 코드는 대기하고 있으므로 결과적으로는 동기로 실행하는 듯한 결과를 얻을 수 있다.
- 장점은 메인스레드를 차단하지 않는다는 점이다.
그렇담 async 메소드의 반환타입은 뭐가 될 수 있을까!!
무조건Task
,Task<TResult>
두 가지만 가능하다.
+) 이 원칙의 예외로 void도 반환타입으로 허용했는데, event handler 때문이다.
Action부터 차곡차곡 알아야 async도 사용할 수 있겠구나~