1. 태스크 생성
개념: 태스크는 비동기 작업을 실행하는 기본 단위입니다. Task.Run 또는 Task.Factory.StartNew를 사용하여 태스크를 생성할 수 있습니다.
주요 메서드:
- Task.Run: 비동기 작업을 실행합니다.
- Task.Wait: 태스크 완료 대기.
- Task.Result: 태스크 결과 반환.
2. 태스크로 기본적인 연산 수행
개념: 배열의 각 요소를 병렬로 처리하거나 CPU 바운드 작업을 병렬화하여 효율성을 높입니다.
주요 메서드:
- Task.Run과 Parallel.ForEach로 데이터를 병렬로 처리.
- 태스크의 결과를 수집하거나 추가 연산 수행.
3. 태스크를 함께 조합
개념: 여러 태스크를 조합하여 의존성을 처리하거나 결과를 결합합니다.
주요 메서드:
- Task.WhenAll: 모든 태스크 완료 후 처리.
- Task.WhenAny: 가장 빠르게 완료된 태스크 결과 반환.
- ContinueWith: 태스크 체이닝.
4. APM 패턴을 태스크로 변환
개념: BeginXXX/EndXXX 메서드를 사용하는 APM 패턴을 태스크 기반으로 변환합니다.
주요 메서드:
- Task.Factory.FromAsync: APM 메서드를 태스크로 변환.
5. EAP 패턴을 태스크로 변환
개념: XXXAsync와 XXXCompleted 이벤트를 사용하는 EAP 패턴을 태스크 기반으로 변환합니다.
주요 기술:
- TaskCompletionSource를 활용하여 작업 완료, 예외, 취소 처리.
6. 취소 옵션 구현
개념: CancellationTokenSource와 CancellationToken을 사용해 작업 중단을 구현합니다.
주요 메서드:
- Cancel: 취소 요청.
- ThrowIfCancellationRequested: 취소 여부 확인.
- OperationCanceledException: 취소 작업 예외.
7. 태스크에서 예외 처리
개념: 태스크에서 발생한 예외를 처리하고, 복구 가능한 시스템을 구현합니다.
주요 기술:
- try-catch와 AggregateException으로 태스크 예외 처리.
- async/await 구문에서 예외 자동 전파.
8. 태스크를 병렬로 실행
개념: 다수의 태스크를 병렬로 실행하여 작업 시간을 단축합니다.
주요 메서드:
- Task.WhenAll: 모든 작업 완료 대기.
- Task.WhenAny: 첫 번째 완료된 작업 처리.
9. TaskScheduler로 태스크 실행을 미세조정
개념: 기본 스케줄러 외에 커스텀 스케줄러를 구현하여 태스크 실행 로직을 제어합니다.
주요 기술:
- TaskScheduler.Default: 기본 스케줄러.
- 커스텀 스케줄러: TaskScheduler를 상속하여 작업 큐와 실행을 재정의.
1. 태스크 생성
1.1 개념 설명
태스크(Task)는 작업 단위를 비동기적으로 실행할 수 있도록 관리하는 기본 단위입니다. .NET의 Task Parallel Library(TPL)를 사용하면 다중 스레드 프로그래밍이 간단하고 효율적으로 처리됩니다. 태스크는 다음과 같은 장점을 제공합니다:
- 작업의 비동기적 실행 및 병렬화
- 비동기 프로그래밍에서 가독성과 유지보수성 향상
- 작업 완료 시 결과를 처리하거나 예외를 캡처할 수 있는 유연성
1.2 기본 샘플 코드
아래는 간단한 태스크 생성 및 실행 코드입니다.
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// 태스크 생성 및 실행
Task task = Task.Run(() =>
{
Console.WriteLine("Task 실행 중...");
Task.Delay(1000).Wait(); // 1초 대기
Console.WriteLine("Task 완료!");
});
task.Wait(); // 태스크 완료 대기
Console.WriteLine("Main 종료.");
}
}
1.3 코드 분석 및 결과 확인
코드 분석
- Task.Run: 새로운 태스크를 실행합니다. 내부적으로 ThreadPool을 사용하여 비동기적으로 작업을 수행합니다.
- Task.Delay: 비동기 작업 실행 중 1초간 대기합니다.
- task.Wait: 메인 스레드가 태스크의 실행이 끝날 때까지 대기합니다.
실행 결과
Task 실행 중...
Task 완료!
Main 종료.
태스크가 완료될 때까지 메인 스레드가 대기하며, 작업 결과를 확인할 수 있습니다.
1.4 고급 확장 샘플 코드
다수의 태스크를 생성하여 병렬로 실행하고 결과를 수집하는 예제를 확장합니다.
using System;
using System.Linq;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// 병렬 태스크 생성
var tasks = Enumerable.Range(1, 5).Select(i => Task.Run(() =>
{
Console.WriteLine($"Task {i} 시작!");
Task.Delay(500 * i).Wait(); // i초 대기
Console.WriteLine($"Task {i} 완료!");
return i * i; // 작업 결과 반환
})).ToArray();
// 모든 태스크가 완료될 때까지 대기 및 결과 수집
Task.WaitAll(tasks);
var results = tasks.Select(t => t.Result).ToArray();
Console.WriteLine("모든 태스크 완료. 결과:");
Console.WriteLine(string.Join(", ", results));
}
}
1.5 코드 분석 및 결과 확인
코드 분석
- Enumerable.Range(1, 5).Select
- 1부터 5까지의 숫자를 순회하며, 각각의 숫자를 기반으로 태스크를 생성합니다.
- Task.Run
- 각 태스크는 병렬로 실행되며, 태스크 내부에서 딜레이 후 결과를 반환합니다.
- Task.WaitAll
- 모든 태스크가 완료될 때까지 메인 스레드가 대기합니다.
- t.Result
- 각 태스크의 결과를 수집합니다.
실행 결과
Task 1 시작!
Task 2 시작!
Task 3 시작!
Task 4 시작!
Task 5 시작!
Task 1 완료!
Task 2 완료!
Task 3 완료!
Task 4 완료!
Task 5 완료!
모든 태스크 완료. 결과:
1, 4, 9, 16, 25
각 태스크가 병렬로 실행되고 작업의 결과(1^2, 2^2, ...)를 수집합니다.
2. 태스크로 기본적인 연산 수행
2.1 개념 설명
태스크는 비동기적으로 연산을 처리할 수 있어 CPU 집약적인 작업이나 IO 작업의 성능을 크게 향상시킬 수 있습니다.
- CPU 바운드 작업: 계산 집약적인 작업. 병렬로 실행해 전체 처리 시간을 단축할 수 있습니다.
- IO 바운드 작업: 파일 읽기/쓰기, 네트워크 요청 같은 작업. 비동기적으로 처리하여 응답성을 개선할 수 있습니다.
이 섹션에서는 태스크를 사용하여 기본적인 연산을 수행하는 방법을 다룹니다.
2.2 기본 샘플 코드
아래는 태스크를 이용해 배열의 요소를 병렬로 제곱하는 간단한 코드입니다.
using System;
using System.Linq;
using System.Threading.Tasks;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5 };
// 각 숫자를 비동기로 제곱 계산
var tasks = numbers.Select(n => Task.Run(() =>
{
Console.WriteLine($"Number: {n}, Square: {n * n}");
return n * n;
})).ToArray();
// 태스크 결과 수집
Task.WaitAll(tasks);
var results = tasks.Select(t => t.Result).ToArray();
Console.WriteLine("결과:");
Console.WriteLine(string.Join(", ", results));
}
}
2.3 코드 분석 및 결과 확인
코드 분석
- Task.Run
- 숫자 배열의 각 요소에 대해 태스크를 실행하여 제곱 값을 계산합니다.
- Task.WaitAll
- 모든 태스크가 완료될 때까지 대기합니다.
- tasks.Select(t => t.Result)
- 각 태스크의 결과(제곱 값)를 추출하여 배열로 변환합니다.
실행 결과
Number: 1, Square: 1
Number: 2, Square: 4
Number: 3, Square: 9
Number: 4, Square: 16
Number: 5, Square: 25
결과:
1, 4, 9, 16, 25
2.4 고급 확장 샘플 코드
이번에는 더 복잡한 연산을 수행하여 병렬 처리의 효과를 극대화합니다. 아래 코드는 소수 판별 작업을 병렬로 실행합니다.
using System;
using System.Linq;
using System.Threading.Tasks;
class Program
{
static void Main()
{
int[] numbers = Enumerable.Range(2, 50).ToArray();
// 병렬로 소수 판별
var tasks = numbers.Select(n => Task.Run(() =>
{
bool isPrime = IsPrime(n);
Console.WriteLine($"Number: {n}, IsPrime: {isPrime}");
return (n, isPrime);
})).ToArray();
// 태스크 결과 수집
Task.WaitAll(tasks);
var primeNumbers = tasks.Where(t => t.Result.isPrime).Select(t => t.Result.n).ToArray();
Console.WriteLine("소수 목록:");
Console.WriteLine(string.Join(", ", primeNumbers));
}
// 소수 판별 함수
static bool IsPrime(int number)
{
if (number <= 1) return false;
for (int i = 2; i * i <= number; i++)
{
if (number % i == 0) return false;
}
return true;
}
}
2.5 코드 분석 및 결과 확인
코드 분석
- Task.Run
- 각 숫자에 대해 태스크를 실행하여 소수인지 여부를 판별합니다.
- IsPrime
- 숫자가 소수인지 판별하는 함수로, 효율적인 나눗셈 검사를 사용합니다.
- tasks.Where(t => t.Result.isPrime)
- 태스크의 결과 중 소수인 숫자만 필터링하여 배열로 변환합니다.
실행 결과
Number: 2, IsPrime: True
Number: 3, IsPrime: True
...
Number: 49, IsPrime: False
소수 목록:
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47
50 이하의 소수 목록을 병렬 연산으로 빠르게 계산했습니다.
3. 태스크를 함께 조합
3.1 개념 설명
태스크 병렬 라이브러리를 사용하면 여러 태스크를 조합하여 복잡한 비동기 흐름을 처리할 수 있습니다. 이를 통해 다음과 같은 작업을 효율적으로 수행할 수 있습니다:
- 태스크 간의 의존성을 설정
- 작업 완료 후 결과를 연결
- 여러 태스크의 결과를 결합
- 병렬로 실행되는 태스크 그룹 관리
Task.WhenAll과 Task.WhenAny 같은 메서드를 활용하면 병렬 작업의 결과를 조합하거나 완료 여부를 제어할 수 있습니다.
3.2 기본 샘플 코드
아래는 여러 태스크를 병렬로 실행하고 결과를 조합하는 간단한 코드입니다.
using System;
using System.Linq;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// 병렬 태스크 실행
Task<int> task1 = Task.Run(() => PerformCalculation(1, 1000));
Task<int> task2 = Task.Run(() => PerformCalculation(2, 1500));
Task<int> task3 = Task.Run(() => PerformCalculation(3, 1200));
// 모든 태스크 완료 후 결과 합산
int[] results = await Task.WhenAll(task1, task2, task3);
int total = results.Sum();
Console.WriteLine("결과 합계: " + total);
}
static int PerformCalculation(int id, int delay)
{
Console.WriteLine($"Task {id} 시작 (대기 시간: {delay}ms)");
Task.Delay(delay).Wait();
int result = id * 10;
Console.WriteLine($"Task {id} 완료 (결과: {result})");
return result;
}
}
3.3 코드 분석 및 결과 확인
코드 분석
- Task.WhenAll
- 세 개의 태스크를 병렬로 실행하고, 모든 태스크가 완료될 때까지 대기하며 결과를 배열로 반환합니다.
- PerformCalculation
- 태스크 ID와 대기 시간을 받아, 작업 완료 후 계산 결과를 반환합니다.
- await
- 비동기 방식으로 태스크 완료를 기다리며 결과를 처리합니다.
실행 결과
Task 1 시작 (대기 시간: 1000ms)
Task 2 시작 (대기 시간: 1500ms)
Task 3 시작 (대기 시간: 1200ms)
Task 1 완료 (결과: 10)
Task 3 완료 (결과: 30)
Task 2 완료 (결과: 20)
결과 합계: 60
세 개의 태스크가 병렬로 실행되고 모든 작업의 결과가 조합됩니다.
3.4 고급 확장 샘플 코드
태스크 간의 의존성을 설정하고, 조건에 따라 태스크를 동적으로 조합하는 예제를 확장합니다.
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// 태스크 실행
Task<int> task1 = Task.Run(() => PerformCalculation("Task 1", 1000));
Task<int> task2 = Task.Run(() => PerformCalculation("Task 2", 1500));
// 첫 번째 태스크가 완료된 후 연계 태스크 실행
Task<int> chainedTask = task1.ContinueWith(prevTask =>
{
Console.WriteLine("Chained Task 실행 중...");
return prevTask.Result * 2;
});
// 모든 태스크 완료 대기 및 결과 조합
int[] results = await Task.WhenAll(task1, task2, chainedTask);
Console.WriteLine("최종 결과: " + string.Join(", ", results));
}
static int PerformCalculation(string name, int delay)
{
Console.WriteLine($"{name} 시작 (대기 시간: {delay}ms)");
Task.Delay(delay).Wait();
int result = delay / 100;
Console.WriteLine($"{name} 완료 (결과: {result})");
return result;
}
}
3.5 코드 분석 및 결과 확인
코드 분석
- Task.ContinueWith
- task1의 결과를 받아 새로운 태스크를 실행합니다. 이는 태스크 간의 의존성을 구현합니다.
- Task.WhenAll
- 병렬 태스크의 모든 결과를 조합하여 최종 결과를 반환합니다.
- await
- 태스크 완료를 비동기적으로 대기합니다.
실행 결과
Task 1 시작 (대기 시간: 1000ms)
Task 2 시작 (대기 시간: 1500ms)
Task 1 완료 (결과: 10)
Chained Task 실행 중...
Task 2 완료 (결과: 15)
최종 결과: 10, 15, 20
task1이 완료된 후 추가 작업(chainedTask)이 실행되며, 모든 태스크 결과가 조합됩니다.
4. APM 패턴을 태스크로 변환
4.1 개념 설명
APM(Asynchronous Programming Model)은 비동기 작업을 수행하는 .NET의 초기 패턴 중 하나로, 일반적으로 BeginXXX/EndXXX 메서드 쌍을 사용합니다.
- APM은 비동기 작업을 제공하지만, 콜백 방식으로 작업을 완료해야 하므로 코드가 복잡하고 가독성이 떨어질 수 있습니다.
- TPL(Task Parallel Library)을 사용하면 APM 기반 메서드를 태스크 기반으로 변환하여 비동기 작업을 보다 효율적으로 관리할 수 있습니다.
Task.Factory.FromAsync를 사용하면 APM 패턴을 태스크로 변환할 수 있습니다.
4.2 기본 샘플 코드
아래는 APM 패턴의 Socket.BeginConnect/Socket.EndConnect 메서드를 태스크로 변환하는 코드입니다.
using System;
using System.Net.Sockets;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string host = "example.com";
int port = 80;
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
// APM 메서드를 태스크로 변환
await Task.Factory.FromAsync(
socket.BeginConnect,
socket.EndConnect,
host,
port,
null);
Console.WriteLine("소켓 연결 완료!");
}
}
}
4.3 코드 분석 및 결과 확인
코드 분석
- Task.Factory.FromAsync
- APM 메서드 쌍(BeginConnect, EndConnect)을 받아 이를 태스크 기반 메서드로 변환합니다.
- BeginConnect
- 비동기적으로 소켓 연결을 시작합니다.
- EndConnect
- 연결 작업이 완료되었음을 나타냅니다.
- await
- 태스크 기반의 비동기 메서드를 호출하고 연결 작업이 완료될 때까지 대기합니다.
실행 결과
소켓 연결 완료!
호스트와 포트로 연결한 후 완료 메시지를 출력합니다.
4.4 고급 확장 샘플 코드
이번에는 파일 읽기 작업의 APM 메서드를 태스크 기반으로 변환합니다.
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string filePath = "example.txt";
byte[] buffer = new byte[1024];
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true))
{
// APM 메서드를 태스크로 변환
int bytesRead = await Task<int>.Factory.FromAsync(
fs.BeginRead,
fs.EndRead,
buffer,
0,
buffer.Length,
null);
Console.WriteLine("파일에서 읽은 데이터:");
Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, bytesRead));
}
}
}
4.5 코드 분석 및 결과 확인
코드 분석
- FileStream.BeginRead
- 파일 스트림에서 데이터를 비동기적으로 읽기 시작합니다.
- FileStream.EndRead
- 읽기 작업이 완료되면 읽은 바이트 수를 반환합니다.
- Task<int>.Factory.FromAsync
- APM 메서드를 태스크로 변환합니다. 변환된 태스크는 읽은 바이트 수를 반환합니다.
실행 결과
파일에서 읽은 데이터:
Hello, APM to Task!
파일의 내용을 비동기적으로 읽은 후 텍스트를 출력합니다.
5. EAP 패턴을 태스크로 변환
5.1 개념 설명
EAP(Event-based Asynchronous Pattern)는 이벤트와 콜백을 사용해 비동기 작업을 처리하는 .NET의 초기 비동기 패턴입니다.
- 특징: XXXAsync 메서드와 작업 완료 시 호출되는 XXXCompleted 이벤트를 사용합니다.
- 단점: 이벤트와 콜백의 복잡성 때문에 코드가 관리하기 어렵습니다.
TaskCompletionSource를 사용하면 EAP 기반 메서드를 태스크 기반으로 변환하여 간결하고 가독성 높은 코드를 작성할 수 있습니다.
5.2 기본 샘플 코드
아래는 WebClient.DownloadStringAsync 메서드를 태스크 기반으로 변환하는 코드입니다.
using System;
using System.Net;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string url = "https://example.com";
string content = await DownloadStringAsync(url);
Console.WriteLine("다운로드 완료:");
Console.WriteLine(content);
}
static Task DownloadStringAsync(string url)
{
var tcs = new TaskCompletionSource();
var webClient = new WebClient();
// EAP 이벤트 핸들러 등록
webClient.DownloadStringCompleted += (sender, e) =>
{
if (e.Error != null)
{
tcs.SetException(e.Error); // 예외 처리
}
else if (e.Cancelled)
{
tcs.SetCanceled(); // 작업 취소
}
else
{
tcs.SetResult(e.Result); // 성공 시 결과 설정
}
};
// 비동기 작업 시작
webClient.DownloadStringAsync(new Uri(url));
return tcs.Task;
}
}
5.3 코드 분석 및 결과 확인
코드 분석
- TaskCompletionSource<T>
- 비동기 작업의 결과를 수동으로 설정할 수 있는 도구입니다.
- SetResult, SetException, SetCanceled로 작업의 상태를 제어합니다.
- DownloadStringAsync
- 비동기적으로 문자열을 다운로드하며, 완료 시 DownloadStringCompleted 이벤트가 호출됩니다.
- 이벤트 핸들러
- 작업 완료(SetResult), 예외 발생(SetException), 작업 취소(SetCanceled)를 처리합니다.
실행 결과
다운로드 완료:
<!DOCTYPE html>
<html>...</html>
URL에서 데이터를 다운로드한 후 내용을 출력합니다.
5.4 고급 확장 샘플 코드
이번에는 FileSystemWatcher를 사용하여 파일 변경 이벤트를 태스크 기반으로 변환합니다.
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string filePath = "example.txt";
Console.WriteLine("파일 변경을 기다립니다...");
await WaitForFileChangeAsync(filePath);
Console.WriteLine("파일이 변경되었습니다!");
}
static Task WaitForFileChangeAsync(string filePath)
{
var tcs = new TaskCompletionSource<bool>();
var watcher = new FileSystemWatcher(Path.GetDirectoryName(filePath), Path.GetFileName(filePath))
{
NotifyFilter = NotifyFilters.LastWrite,
EnableRaisingEvents = true
};
// EAP 이벤트 핸들러 등록
FileSystemEventHandler onChanged = null;
onChanged = (sender, e) =>
{
if (e.ChangeType == WatcherChangeTypes.Changed)
{
tcs.TrySetResult(true); // 파일 변경 감지
watcher.Changed -= onChanged; // 이벤트 핸들러 제거
watcher.Dispose(); // 리소스 정리
}
};
watcher.Changed += onChanged;
return tcs.Task;
}
}
5.5 코드 분석 및 결과 확인
코드 분석
- FileSystemWatcher
- 지정된 파일의 변경을 감시하는 객체입니다.
- NotifyFilters.LastWrite를 사용하여 파일 쓰기 작업을 감지합니다.
- TaskCompletionSource
- 파일 변경 이벤트가 발생했을 때 SetResult를 호출하여 태스크를 완료합니다.
- 리소스 관리
- 이벤트 핸들러 제거 및 FileSystemWatcher 객체를 Dispose하여 리소스를 해제합니다.
실행 결과
파일 변경을 기다립니다...
(파일을 수정한 후)
파일이 변경되었습니다!
지정된 파일이 변경될 때까지 대기하고, 변경이 감지되면 메시지를 출력합니다.
6. 취소 옵션 구현
6.1 개념 설명
비동기 작업을 실행할 때 특정 조건에서 작업을 중단해야 하는 경우가 있습니다. TPL에서는 **CancellationToken**과 **CancellationTokenSource**를 사용하여 취소 기능을 구현할 수 있습니다.
- CancellationTokenSource: 작업 취소 요청을 생성하는 객체입니다.
- CancellationToken: 취소 요청을 전달받아 작업이 중단되었는지 확인합니다.
- 비동기 작업 내부에서 주기적으로 **token.ThrowIfCancellationRequested**를 호출하여 취소 여부를 확인합니다.
6.2 기본 샘플 코드
아래는 취소 토큰을 사용하여 비동기 작업을 취소하는 간단한 예제입니다.
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var cts = new CancellationTokenSource();
Task task = RunTaskAsync(cts.Token);
Console.WriteLine("3초 후 작업을 취소합니다...");
await Task.Delay(3000);
cts.Cancel();
try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("작업이 취소되었습니다!");
}
finally
{
cts.Dispose();
}
}
static async Task RunTaskAsync(CancellationToken token)
{
for (int i = 1; i <= 10; i++)
{
token.ThrowIfCancellationRequested(); // 취소 요청 확인
Console.WriteLine($"작업 진행 중: {i * 10}%");
await Task.Delay(1000); // 1초 대기
}
Console.WriteLine("작업 완료!");
}
}
6.3 코드 분석 및 결과 확인
코드 분석
- CancellationTokenSource
- 취소 요청을 생성하는 객체로, 작업을 중단하기 위해 cts.Cancel()을 호출합니다.
- CancellationToken
- 태스크 내부에서 ThrowIfCancellationRequested를 호출하여 취소 상태를 확인합니다.
- OperationCanceledException
- 작업이 취소되었을 때 발생하는 예외로, 이를 통해 취소된 상태를 처리할 수 있습니다.
실행 결과
3초 후 작업을 취소합니다...
작업 진행 중: 10%
작업 진행 중: 20%
작업 진행 중: 30%
작업이 취소되었습니다!
3초 후 작업이 중단되며, OperationCanceledException을 통해 취소 메시지가 출력됩니다.
6.4 고급 확장 샘플 코드
여러 태스크를 병렬로 실행하면서 특정 조건에서 모든 태스크를 취소하는 코드입니다.
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var cts = new CancellationTokenSource();
var tasks = Enumerable.Range(1, 5).Select(i => RunTaskAsync(i, cts.Token)).ToArray();
Console.WriteLine("5초 후 모든 작업을 취소합니다...");
await Task.Delay(5000);
cts.Cancel();
try
{
await Task.WhenAll(tasks);
}
catch (OperationCanceledException)
{
Console.WriteLine("모든 작업이 취소되었습니다!");
}
finally
{
cts.Dispose();
}
}
static async Task RunTaskAsync(int id, CancellationToken token)
{
for (int i = 1; i <= 10; i++)
{
token.ThrowIfCancellationRequested(); // 취소 요청 확인
Console.WriteLine($"태스크 {id}: 진행 {i * 10}%");
await Task.Delay(1000); // 1초 대기
}
Console.WriteLine($"태스크 {id}: 완료!");
}
}
6.5 코드 분석 및 결과 확인
코드 분석
- Enumerable.Range
- 5개의 태스크를 병렬로 실행합니다.
- Task.WhenAll
- 모든 태스크가 완료되기를 대기합니다. 취소 요청 시 OperationCanceledException을 발생시킵니다.
- CancellationTokenSource.Cancel
- 모든 태스크에서 취소 요청을 수신합니다.
실행 결과
5초 후 모든 작업을 취소합니다...
태스크 1: 진행 10%
태스크 2: 진행 10%
...
태스크 5: 진행 50%
모든 작업이 취소되었습니다!
5초 후 취소 요청이 전달되고, 모든 태스크가 중단됩니다.
7. 태스크에서 예외 처리
7.1 개념 설명
태스크 병렬 라이브러리(TPL)를 사용할 때 작업 중 발생한 예외를 안전하게 처리하는 방법이 중요합니다. 태스크는 예외를 캡슐화하여 호출자가 명시적으로 처리할 수 있도록 합니다.
- AggregateException: 태스크에서 발생한 모든 예외를 포함하는 예외 객체입니다.
- 예외 처리 방식:
- **try-catch**를 사용하여 개별 태스크 예외를 처리합니다.
- Task.Wait 또는 **Task.WaitAll**에서 발생한 예외를 처리합니다.
- async/await 구문을 사용하면 예외가 자동으로 전파됩니다.
7.2 기본 샘플 코드
아래는 태스크에서 발생한 단일 예외를 처리하는 코드입니다.
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
try
{
await RunTaskAsync();
}
catch (Exception ex)
{
Console.WriteLine($"예외 발생: {ex.Message}");
}
}
static async Task RunTaskAsync()
{
Console.WriteLine("태스크 시작");
await Task.Delay(1000);
throw new InvalidOperationException("태스크에서 예외 발생!");
}
}
7.3 코드 분석 및 결과 확인
코드 분석
- try-catch
- Main 메서드에서 태스크 실행 중 발생한 예외를 처리합니다.
- throw
- 태스크 내부에서 명시적으로 예외를 발생시킵니다.
- await
- 예외는 호출 스택을 따라 상위 호출자에게 전달됩니다.
실행 결과
태스크 시작
예외 발생: 태스크에서 예외 발생!
태스크 내부에서 발생한 예외가 호출자에게 전달되며 메시지가 출력됩니다.
7.4 고급 확장 샘플 코드
이번에는 여러 태스크를 병렬로 실행하면서 각각의 태스크에서 발생한 예외를 처리하는 예제를 확장합니다.
using System;
using System.Linq;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var tasks = new[]
{
Task.Run(() => PerformTask(1)),
Task.Run(() => PerformTask(2)),
Task.Run(() => PerformTask(3))
};
try
{
await Task.WhenAll(tasks);
}
catch (AggregateException ex)
{
foreach (var innerEx in ex.InnerExceptions)
{
Console.WriteLine($"예외 발생: {innerEx.Message}");
}
}
}
static void PerformTask(int id)
{
Console.WriteLine($"태스크 {id} 시작");
if (id == 2)
{
throw new InvalidOperationException($"태스크 {id}에서 예외 발생!");
}
Task.Delay(1000).Wait();
Console.WriteLine($"태스크 {id} 완료");
}
}
7.5 코드 분석 및 결과 확인
코드 분석
- Task.WhenAll
- 모든 태스크가 완료될 때까지 대기하며, 하나 이상의 태스크에서 예외가 발생하면 AggregateException을 던집니다.
- AggregateException.InnerExceptions
- 여러 태스크의 예외를 캡슐화한 객체로, 개별 예외에 접근할 수 있습니다.
- 예외 필터링
- 각 태스크의 예외를 분리하여 개별적으로 처리합니다.
실행 결과
태스크 1 시작
태스크 2 시작
태스크 3 시작
예외 발생: 태스크 2에서 예외 발생!
태스크 2에서 예외가 발생했지만, 나머지 태스크는 영향을 받지 않습니다.
8. 태스크를 병렬로 실행
8.1 개념 설명
태스크 병렬 실행은 다중 스레드를 사용하여 작업을 동시에 수행함으로써 성능을 크게 향상할 수 있는 기술입니다.
- 병렬 실행의 장점:
- CPU 집약적인 작업을 효율적으로 분산 처리
- IO 작업의 대기 시간을 줄여 전체 처리 시간을 단축
- 주요 메서드:
- Task.WhenAll: 모든 태스크를 병렬로 실행하고 완료될 때까지 대기
- Task.WhenAny: 가장 먼저 완료된 태스크의 결과를 반환
8.2 기본 샘플 코드
아래는 Task.WhenAll을 사용하여 3개의 태스크를 병렬로 실행하는 코드입니다.
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Task task1 = Task.Run(() => PerformTask(1, 1000));
Task task2 = Task.Run(() => PerformTask(2, 1500));
Task task3 = Task.Run(() => PerformTask(3, 1200));
await Task.WhenAll(task1, task2, task3);
Console.WriteLine("모든 태스크가 완료되었습니다!");
}
static void PerformTask(int id, int delay)
{
Console.WriteLine($"태스크 {id} 시작");
Task.Delay(delay).Wait();
Console.WriteLine($"태스크 {id} 완료");
}
}
8.3 코드 분석 및 결과 확인
코드 분석
- Task.WhenAll
- 모든 태스크를 병렬로 실행하고, 모든 태스크가 완료될 때까지 비동기적으로 대기합니다.
- Task.Run
- 태스크를 시작하며, 내부적으로 스레드 풀을 활용합니다.
- Task.Delay
- 태스크가 특정 시간 동안 대기하도록 만듭니다.
실행 결과
태스크 1 시작
태스크 2 시작
태스크 3 시작
태스크 1 완료
태스크 3 완료
태스크 2 완료
모든 태스크가 완료되었습니다!
태스크가 병렬로 실행되고, 작업이 완료된 순서대로 결과가 출력됩니다.
8.4 고급 확장 샘플 코드
이번에는 태스크 중 가장 먼저 완료된 작업을 처리하는 Task.WhenAny를 활용한 예제를 확장합니다.
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Task<int> task1 = PerformTaskAsync(1, 3000);
Task<int> task2 = PerformTaskAsync(2, 2000);
Task<int> task3 = PerformTaskAsync(3, 1000);
Task<int> completedTask = await Task.WhenAny(task1, task2, task3);
Console.WriteLine($"가장 먼저 완료된 태스크 결과: {await completedTask}");
}
static async Task<int> PerformTaskAsync(int id, int delay)
{
Console.WriteLine($"태스크 {id} 시작");
await Task.Delay(delay);
Console.WriteLine($"태스크 {id} 완료");
return id * 10; // 태스크 결과 반환
}
}
8.5 코드 분석 및 결과 확인
코드 분석
- Task.WhenAny
- 태스크 중 가장 먼저 완료된 태스크를 반환하며, 나머지 태스크는 계속 실행됩니다.
- PerformTaskAsync
- 각 태스크는 비동기적으로 실행되며, 일정 시간이 지나면 결과를 반환합니다.
- await completedTask
- Task.WhenAny가 반환한 태스크의 결과를 비동기적으로 대기합니다.
실행 결과
태스크 1 시작
태스크 2 시작
태스크 3 시작
태스크 3 완료
가장 먼저 완료된 태스크 결과: 30
태스크 2 완료
태스크 1 완료
가장 빠르게 완료된 태스크 결과를 우선 처리하며, 나머지 태스크는 별도로 완료됩니다.
9. TaskScheduler로 태스크 실행을 미세조정
9.1 개념 설명
TaskScheduler는 태스크 실행 방식과 스레드 관리를 제어할 수 있는 기능을 제공합니다.
- 기본적으로 .NET의 기본 스케줄러는 스레드 풀을 사용하여 태스크를 실행합니다.
- 커스텀 스케줄러를 구현하면 태스크 실행 순서, 스레드 할당, 동작 우선순위 등을 세부적으로 제어할 수 있습니다.
주요 개념
- TaskScheduler.Default: .NET이 제공하는 기본 스케줄러로, 작업을 스레드 풀에서 실행합니다.
- TaskScheduler.Current: 현재 실행 중인 작업이 사용하는 스케줄러를 나타냅니다.
- 커스텀 TaskScheduler: 태스크 실행 로직을 재정의하려면 TaskScheduler 클래스를 상속하고 필요한 메서드를 재정의합니다.
9.2 기본 샘플 코드
아래는 기본 TaskScheduler를 사용하는 예제입니다.
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine("태스크 실행 시작");
TaskScheduler scheduler = TaskScheduler.Default;
var task = Task.Factory.StartNew(() =>
{
Console.WriteLine($"태스크 실행 중 (스케줄러: {scheduler.GetType().Name})");
}, TaskCreationOptions.None, scheduler);
await task;
Console.WriteLine("태스크 실행 완료");
}
}
9.3 코드 분석 및 결과 확인
코드 분석
- TaskScheduler.Default
- 기본 스케줄러를 사용하여 태스크를 실행합니다.
- Task.Factory.StartNew
- 명시적으로 태스크 스케줄러를 지정하여 태스크를 시작합니다.
- await
- 태스크 완료를 비동기적으로 대기합니다.
실행 결과
태스크 실행 시작
태스크 실행 중 (스케줄러: ThreadPoolTaskScheduler)
태스크 실행 완료
태스크는 기본 스레드 풀 스케줄러에서 실행됩니다.
9.4 고급 확장 샘플 코드
커스텀 TaskScheduler를 구현하여 태스크 실행 로직을 재정의하는 고급 예제입니다.
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
class CustomScheduler : TaskScheduler
{
private readonly BlockingCollection<Task> taskQueue = new BlockingCollection<Task>();
public CustomScheduler()
{
// 별도 스레드에서 태스크 실행
var thread = new Thread(ExecuteTasks) { IsBackground = true };
thread.Start();
}
protected override IEnumerable<Task> GetScheduledTasks() => taskQueue;
protected override void QueueTask(Task task)
{
Console.WriteLine("태스크가 큐에 추가되었습니다.");
taskQueue.Add(task);
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
Console.WriteLine("태스크를 인라인으로 실행합니다.");
return TryExecuteTask(task);
}
private void ExecuteTasks()
{
foreach (var task in taskQueue.GetConsumingEnumerable())
{
TryExecuteTask(task);
}
}
}
class Program
{
static async Task Main()
{
var scheduler = new CustomScheduler();
var task = Task.Factory.StartNew(() =>
{
Console.WriteLine("커스텀 스케줄러에서 실행 중...");
}, CancellationToken.None, TaskCreationOptions.None, scheduler);
await task;
Console.WriteLine("태스크 완료");
}
}
9.5 코드 분석 및 결과 확인
코드 분석
- CustomScheduler
- BlockingCollection<Task>를 사용하여 태스크를 큐에 추가하고 별도의 스레드에서 실행합니다.
- QueueTask
- 태스크가 실행 대기열에 들어갔을 때 호출됩니다.
- TryExecuteTaskInline
- 태스크를 즉시 실행 가능한 경우 인라인으로 실행합니다.
- Task.Factory.StartNew
- 커스텀 스케줄러를 지정하여 태스크를 시작합니다.
실행 결과
태스크가 큐에 추가되었습니다.
커스텀 스케줄러에서 실행 중...
태스크 완료
커스텀 스케줄러가 태스크를 실행하고, 태스크 완료 메시지를 출력합니다.
'C# > 쓰레드' 카테고리의 다른 글
1. C# 스레드 생성과 병렬 처리 이해 (0) | 2025.01.24 |
---|---|
C# 태스크(Task)와 비동기 프로그래밍: 생성, 연속 작업, 부모-자식 관계 이해 (0) | 2025.01.20 |
C# Task 기본 동작과 스레드 실행 시나리오 이해 (12) | 2025.01.17 |
C# TPL: 태스크 생성 방식과 실행 흐름 완벽 이해 (1) | 2025.01.17 |
C# `Thread`와 `ThreadPool`의 차이점 및 델리게이트 비동기 호출 이해 (16) | 2025.01.16 |
C# 스레드풀(Thread Pool): 개념, 사용법, 및 예제 코드 설명 (1) | 2025.01.16 |
C# SemaphoreSlim: 멀티스레드 리소스 접근 제어 예제와 설명 (1) | 2025.01.16 |
C# 스레드 제어: Semaphore와 SemaphoreSlim의 차이와 사용법 (2) | 2025.01.16 |