Task.Delay (vs Thread.Sleep)
개요
Task.Delay를 통해 진행중인 Task를 일시정지 할 수 있다.
public static async Task<int> DoSomethingAsync()
{
await Task.Delay(100); //100ms 이상 대기한다.
return Thread.CurrentThread.ManagedThreadId;
}
이를 이용하여 소프트 타임아웃을 구현할 수 있다.
public static async Task<SomeResult> TaskWithSoftTimeout()
{
Task<SomeResult> someTask = DoSomethingAsync();
var timeoutTask = Task.Delay(100);
//둘 중 하나가 완료되길 대기
var completeTask = Task.WhenAny(someTask, timeoutTask);
if (completeTask == timeoutTask)
return null;
return someTask.Result;
}
그러나 실행시킬 수 있는 작업이 취소할 수 있는 작업이라면 CancellationToken을 이용한 방법이 더 바람직하다.
위 코드에서 timeoutTask가 먼저 완료되어도 someTask는 계속 수행되기 때문이다.
await Task.Delay vs Thread.Sleep
그렇다면 await Task.Delay 대신 Thread.Sleep을 쓰면 어떨까?
Thread.Sleep는 현재 스레드를 블락시킨다. 따라서 새로운 쓰레드가 생성되거나, 컨텍스트 스위칭이 발생할 확률이 높아진다.
반면 await Task.Delay는 그렇지 않다. Task.Delay는 단지 주어진 시간이후에 완료되는 Task일 뿐이며, await는 단지 Task가 완료될때까지 대기할 뿐이다. (쓰레드를 블락시키지 않는다.)
따라서 너무 당연하게도, 비동기 코드를 작성할때는 await Task.Delay를 쓰는 것이 바람직하다.
쓰레드 풀에서 이런 차이를 극명히 느낄 수 있다. 가령 지금 내 컴퓨터 환경에서 쓰레드 풀은 12~13개의 쓰레드만을 사용한다.
List<Task<int>> tasks = new();
Stopwatch sw = new();
sw.Start();
//100개의 태스크를 실행한다.
for (int i = 0; i < 100; ++i)
{
var task = Task.Run(DoSomethingAsync);
tasks.Add(task);
}
//모두 완료될때까지 대기한다.
Task.WaitAll(tasks.ToArray());
sw.Stop();
//쓰레드 풀이 몇 개의 쓰레드를 가졌는지 출력
Console.WriteLine($"ThreadCount : {ThreadPool.ThreadCount}");
//시간이 얼마나 걸렸는지 출력
Console.WriteLine($"time taken {sw.ElapsedMilliseconds}ms");
위 코드는 DoSomethingAsync를 100번 수행한뒤 수행 시간을 출력한다.
public static async Task<int> DoSomethingAsync()
{
Thread.Sleep(100);
return Thread.CurrentThread.ManagedThreadId;
}
Thread.Sleep을 사용한 경우 아래와 같은 결과가 나온다.
ThreadCount : 13 time taken 909ms |
만약 쓰레드 풀이 100개의 쓰레드를 생성하였다면 수행 시간이 100ms대였겠지만, 13개의 쓰레드만을 사용하였기 때문에 909ms가 나왔다. 동기적인 코드였다면 10000ms 이상 소요되었을 것이다.
public static async Task<int> DoSomethingAsync()
{
await Task.Delay(100);
return Thread.CurrentThread.ManagedThreadId;
}
await Task.Delay를 사용한 경우 아래와 같은 결과가 나온다.
ThreadCount : 12 time taken 111ms |
Thread.Sleep보다 훨씬 빨리 완료되었다. 그 이유는 현재 쓰레드를 블락하지 않았기 때문에 현재 쓰레드가 다른 작업을 이어서 처리할 수 있기 때문.
Task.Delay.wait vs Thread.Sleep
Task.Delay(1).Wait();
Thread.Sleep(1);
이 두개는 외부에서 보기에는 거의 비슷하다고 볼 수 있다. 다만 Task.Wait는 단순히 Sleep가 아닌 별도의 로직이 있다. (상황에 따라 SpinWait를 하기도, Thread.Yield를 호출하기도 함)
요약
비동기 코드에서는 Thread.Sleep쓰지마라