언어/C#

CLR 스레딩3 - 태스크(Task)

tsyang 2022. 4. 24. 21:06

2022.04.17 - [언어/C#] - CLR 스레드 단순 계산 작업

 

태스크


앞선 글에서 계산 중심의 비동기 작업을 주행하기 위해 ThreadPool 의 QueueUserWorkItem을 호출하는 방식을 소개헀다. 그러나 이 방법은 작업 완료 시점과 작업 수행의 결과를 얻을 수 있는 방법을 제공하지 않고 있다는 한계가 있다.

 

그래서 생긴게 태스크(tasks) 라는 개념이다. 

 

//ThreadPool을 이용한 방식
ThreadPool.QueueUserWorkItem(obj => Sum(1000));

//Task를 이용한 방식1
var task = new Task(() => Sum(1000));
task.Start();

//Task를 이용한 방식2
Task.Run(() => Sum(1000));

 

 

태스크의 결과 얻기

태스크가 완료될 때까지 대기하였다가 결과를 얻을 수 있다.

var t = new Task<int>(()=>Sum(100));

//태스크 수행
t.Start();

//'명시적' 대기
t.Wait(); //타임아웃이나 CancellationToken을 줄 수 있다.

Console.WriteLine($"Sum is {t.Result}");

 

그러나 task의 Result 속성은 내부적으로 Wait를 호출하기 때문에 Wait를 별도로 호출하지 않아도 같은 결과를 얻을 수 있다.

 

var t = new Task<int>(()=>Sum(100));

//태스크 수행
t.Start();

//Result 속성은 내부적으로 Wait를 호출한다.
Console.WriteLine($"Sum is {t.Result}");

 

만약 다수의 Task 객체를 배열로 만들어 사용할 경우 WaitAny나 WaitAll같은 정적 메서드를 통해 태스크 일부 혹은 전체가 완료될 때 까지 기다릴 수 있다.

 

Wait 메서드를 호출한 경우 TaskScheduler의 동작 방식에 따라 Wait를 호출한 스레드가 직접 태스크를 수행할 수도 있다. 이 경우 성능 개선을 기대해볼 수 있으나, Lock을 소유한 스레드가 Wait메서드를 호출하고 태스크 내에서 동일한 Lock을 획득하려 하는 경우 데드락이 발생할 수 있기에 주의해야 한다.

 

 

예외

만약 태스크가 수행 중에 처리되지 않은 예외를 유발하는 경우, 바로 예외를 발생시키지 않고 컬렉션에 예외를 저장 한 후, 스레드가 스레드 풀로 반납된다. 이후 Wait메서드를 호출하거나 Result 속성 값을 가져오려 하면 그때 예외(AggregateException)가 발생한다.

 

 

태스크 취소

태스크도 토큰을 이용하여 취소할 수 있다.

private static int Sum(CancellationToken ct, int n)
{
    int sum = 0;
    for (; n > 0; --n)
    {
        //토큰의 Cancel메서드가 호출된 경우 예외를 던진다.
        ct.ThrowIfCancellationRequested();

        sum += n;
    }
    return sum;
}
var cts = new CancellationTokenSource();
var t = new Task<int>(()=>Sum(cts.Token, 10000));

t.Start();

cts.Cancel();

try
{
    Console.WriteLine($"Sum is {t.Result}");
}
catch (AggregateException e)
{
    Console.WriteLine(e);
}

 

 

태스크 완료시 다른 태스크 수행하기

가능한 스레드는 블로킹하지 않는게 좋다. 태스크 수행 중 Wait나 Result 속성을 호출하면 거의 항상 스레드 풀 내에 새로운 스레드가 생기게 된다. (당연히 성능에 안 좋음). 

 

더 좋은 방법은 ContinueWith 메서드를 사용하는 것이다.

var t = new Task<int>(()=>Sum(10000));

//태스크 수행
t.Start();

Task cwt = t.ContinueWith(task => Console.WriteLine($"Sum is {task.Result}"));

Console.ReadLine(); //콘솔 유지용

 

이렇게 되면 태스크를 호출한 스레드는 더 이상 태스크를 기다릴 필요 없이 다른 작업을 수행할 수 있다. 

 

Task 객체는 내부적으로 ContinueWith 태스크를 컬렉션에 저장한다. 따라서 해당 메서드를 여러 번 호출할 수 있다. 또한 TaskContinuationOption을 이용하여 태스크가 정상적으로 완료되었거나, 실패하는 등의 조건에만 태스크를 추가로 수행하도록 설정해줄 수 있다. 

 

기타

태스크는 부모/자식 관계를 가질 수 있다. 부모 태스크는 자식 태스크들이 모두 작업을 완료해야 자신도 작업을 완료한 것으로 간주하게 된다.

 

태스크 객체는 태스크의 상태를 나타내기 위한 여러 필드를 가지고 있다. 그렇기에 불필요한 메모리를 소비할 여지가 많다. 따라서 태스크가 제공하는 기능이 필요한 게 아니라면 ThreadPool.QueueUserWorkItem을 사용하는 것도 좋은 방법이다.

 


참고 : 제프리 리처, CLR via C# 4판 , 비제이퍼블릭, 27장. 계산 중심의 비동기 작업  [814~829p]

'언어 > C#' 카테고리의 다른 글

단순동기화2 - 유저 모드 동기화  (0) 2022.05.06
CLR 단순 동기화1  (0) 2022.05.01
CLR 스레딩2 단순 계산 작업  (1) 2022.04.17
CLR 스레딩1 기본  (0) 2022.04.10
런타임 Serialization - 1  (1) 2022.04.03