
빌 와그너, ⌜More Effective C# 2판⌟, 김완섭 옮김, 한빛미디어, 2019
이 포스팅은 ⌜More Effective C# 2판⌟의 내용을 기반으로 작성되었습니다.
태스크와 컨티뉴에이션에 대한 설명 출처
: https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/chaining-tasks-by-using-continuation-tasks
연속 작업 : Task 선행 작업이 완료되면 이어서 수행되는 작업
Task.TaskStatus 값이 WaitingForActivation 상태일때 생성되는 작업
단일 선행 사건에 대한 연속 작업 생성 방법
using System;
using System.Threading.Tasks;
public class SimpleExample
{
public static async Task Main()
{
// Declare, assign, and start the antecedent task.
Task<DayOfWeek> taskA = Task.Run(() => DateTime.Today.DayOfWeek);
// Execute the continuation when the antecedent finishes.
await taskA.ContinueWith(antecedent => Console.WriteLine($"Today is {antecedent.Result}."));
}
}
여러 선행 항목에 대한 연속 작업 생성 방법
여러 작업이 모두 끝났을때 연속 작업을 수행하는 경우
여러 작업 중 하나라도 끝났을 때 연속 작업을 수행하는 경우
예시 코드
using System.Collections.Generic;
using System;
using System.Threading.Tasks;
public class WhenAllExample
{
public static async Task Main()
{
var tasks = new List<Task<int>>();
for (int ctr = 1; ctr <= 10; ctr++)
{
int baseValue = ctr;
tasks.Add(Task.Factory.StartNew(b => (int)b! * (int)b, baseValue));
}
var results = await Task.WhenAll(tasks);
int sum = 0;
for (int ctr = 0; ctr <= results.Length - 1; ctr++)
{
var result = results[ctr];
Console.Write($"{result} {((ctr == results.Length - 1) ? "=" : "+")} ");
sum += result;
}
Console.WriteLine(sum);
}
}
// The example displays the similar output:
// 1 + 4 + 9 + 16 + 25 + 36 + 49 + 64 + 81 + 100 = 385
Task.WhenAny()는 호출할때마다 새로운 태스크를 생성하기 때문에 관리해야할 태스크가 늘어날 수록 더 많은 자원을 필요로해 효율이 급격히 떨어진다.
대안으로 TaskCompletionSource 클래스를 생각할 수 있다.
public static Task<T>[] OrderByCompletion<T>(this IEnumerable<Task<T>> tasks)
{
// 여러 번 열거되기 때문에 리스트에 복사한다.
var sourceTasks = tasks.ToList();
// 출발지 태스크와 목적지 태스크를 할당한다.
// 각 목적지 태스크는 각 TaskCompletionSource와 연결된다.
var completionSources = new TaskCompletionSource<T>[sourceTasks.Count];
var outputTasks = new Task<T>[completionSources.Length];
for(int i = 0; i < completionSources.Length; i++)
{
completionSources[i] = new TaskCompletionSource<T>();
outputTasks[i] = completionSources[i].Task;
}
// 첫 번째 마법
// 각 태스크가 가지고 있는 컨티뉴에이션은
// 자신의 결과를 completionSources 배열의 다음 위치에 저장한다.
int nextTaskindex = -1;
Action<Task<T>> continuation = completed =>
{
var bucket = completionSources[Interlocked.Increment(ref nextTaskIndex)];
if(completed.IsCompleted)
bucket.TrySetResult(completed.Result);
else if(completed.IsFaulted)
bucket.TrySetException(completed.Exception);
};
// 두 번째 마법
// 각 입력 태스크에 컨티뉴에이션을 설정하여 결과 태스크와 연결한다.
// 각 태스크가 끝나면 다음 빈 공간을 사용한다.
foreach (var inputTask in sourceTasks)
{
inputTask.ContinueWith(continuation,
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
return outputTask;
}
태스크 10개를 실행하는 예라고 했을때, 태스크 3, 7, 2, 0 ... 순서로 끝난다고 가정해보자.
태스크 3이 끝나고 컨티뉴에이션이 실행되어 Task 결과를 목적지 배열의 슬롯 0에 배치한다.
그 다음 태스크 7이 끝나면 목적지 슬롯 1에 배치한다.
태스크 완료 끝까지 반복한다.
Task 라이브러리에서 찾을 수 있는 이런 확장 기능을 이용하면 태스크가 끝난 시점에 취할 수 있는 액션을 지정할 수 있다.
또한 매우 비효율적인 코드를 가독성 높은 코드로 수정할 수 있다.