https://www.oreilly.com/library/view/concurrency-in-c/9781492054498/
Concurrency in C# Cookbook, 2nd Edition
If you’re one of many developers still uncertain about concurrent and multithreaded development, this practical cookbook will change your mind. With more than 85 code-rich recipes in this updated second … - Selection from Concurrency in C# Cookbook, 2n
www.oreilly.com
Example
async Task DoSomethingAsync() { int value = 1; // Context 저장 // null ? SynchronizationContext : TaskScheduler // ASP.NET Core는 별도의 요청 컨텍스트가 아닌 threadpool context 사용 await Task.Delay(TimeSpan.FromSeconds(1)); value *= 2; await Task.Delay(TimeSpan.FromSeconds(1)); Trace.WriteLine(value); }
항상 코어 '라이브러리' 메서드 안에서 ConfigureAwait를 호출하고,
필요할 때만 다른 외부 '사용자 인터페이스' 메서드에서 컨택스트를 재개하는 것이 좋다.
async Task DoSomethingAsync() { int value = 1; await Task.Delay(TimeSpan.FromSeconds(1)) .ConfigureAwait(false); // Threadpool thread에서 실행을 재개한다. value *= 2; await Task.Delay(TimeSpan.FromSeconds(1)) .ConfigureAwait(false); Trace.WriteLine(value); }
ValueTask<T>
- 메모리 내 캐시에서 결과를 읽을 수 있는 등 메모리 할당을 줄일 수 있는 형식
Task 인스턴스를 만드는 방법
- CPU가 실행해야 할 실제 코드를 나타내는 계산 작업은 Task.Run으로 생성
- 특정 스케줄러에서 실행해야 한다면 TaskFactory.StartNew로 생성
- 이벤트 기반 작업은 TaskCompletionSource<TResult>
(대부분 I/O 작업은 TaskCompletionSource<TResult> 사용)
오류 처리
async Task TrySomethingAsync() { // 예외는 Task에서 발생한다. var task = PossibleExceptionAsync(); try { // 여기서 예외 발생 await task; } catch (NotSupportedException ex) { // 이렇게 잡힌 예외는 자체적으로 적절한 스택 추적(Stack trace)을 보존하고 있으며 // 따로 TargetInvocationException이나 AggregateException으로 쌓여 있지 않다. LogException(ex); throw; } }
Deadlock
async Task WaitAsync() { // 3. 현재 context 저장 await Task.Delay(TimeSpan.FromSeconds(1)); // ... // 3. 저장된 context 안에서 재개 시도 // Deadlock 메서드의 2. task.Wait()에서 차단된 thread // context는 한 번에 하나의 thread만 허용하므로 재개할 수 없음 } void Deadlock() { // 1. 지연 시작 var task = WaitAsync(); // 2. 동기적으로 차단하고 async 메서드의 완료 대기 task.Wait(); }
위의 코든느 UI 컨텍스트나 ASP.NET 클래식 컨텍스트에서 호출하면 교착 상태에 빠진다.
ConfigureAwait(false)로 해결
async Task WaitAsync() { // 3. 현재 context 저장 await Task.Delay(TimeSpan.FromSeconds(1)) .ConfigureAwait(false); // ... // 3. Threadpool thread에서 재개 } void Deadlock() { // 1. 지연 시작 var task = WaitAsync(); // 2. 동기적으로 차단하고 async 메서드의 완료 대기 task.Wait(); }
https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/
Asynchronous programming in C#
An overview of the C# language support for asynchronous programming using async, await, Task, and Task
learn.microsoft.com
Task-based Asynchronous Pattern (TAP): Introduction and overview
Learn about the Task-based Asynchronous Pattern (TAP), and compare it to the legacy patterns: Asynchronous Programming Model (APM) and Event-based Asynchronous Pattern (EAP).
learn.microsoft.com
Exponential backoff
async Task<string> DownloadStringWithRetries(HttpClient client, string uri) { TimeSpan nextDelay = TimeSpan.FromSeconds(1); for (int i = 0; i < 3; ++i) { try { return await client.GetStringAsync(uri); } catch { } await Task.Delay(nextDelay); nextDelay = nextDelay + nextDelay; } // 오류를 전파할 수 있게 마지막으로 한 번 더 시도 return await client.GetStringAsync(uri); }
Soft timeout
async Task<string> DownloadStringWithTimeout(HttpClient client, string uri) { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); Task<string> downloadTask = client.GetStringAsync(uri); Task timeoutTask = Task.Delay(Timeout.InfiniteTimeSpan, cts.Token); Task completedTask = await Task.WhenAny(downloadTask, timeoutTask); if (completedTask == timeoutTask) { // WARNING: downloadTask는 여전히 동작한다. return null; } return await downloadTask; }
타임아웃이 지나면 실행을 중단해야 할 때
async Task IssueTimeoutAsync() { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); CancellationToken token = cts.Token; await Task.Delay(TimeSpan.FromSeconds(10), token); } async Task IssueTimeoutAsync() { using var cts = new CancellationTokenSource(); CancellationToken token = cts.Token; cts.CancelAfter(TimeSpan.FromSeconds(3)); await Task.Delay(TimeSpan.FromSeconds(10), token); }
비동기 시그니처를 사용해서 동기 메서드 구현
interface IMyAsync { Task<int> GetValueAsync(CancellationToken token); Task DoSomethingAsync(); } class MySync : IMyAsync { // 자주 사용하는 작업 결과라면 미리 만들어 놓고 쓴다. private static readonly Task<int> ZeroTask = Task.FromResult(0); public Task<int> GetValueAsync(CancellationToken token) { if (token.IsCancellationRequested) return Task.FromCanceled<int>(token); return Task.FromResult(10); } public Task<T> NotImplementedAsync() { return Task.FromException<T>(new NotImplementedException()); } protected void DoSomethingSynchronously() { } public Task DoSomethingAsync() { try { DoSomethingSynchronously(); return Task.CompletedTask; } catch (Exception ex) { return Task.FromException(ex); } } }
진행 상황 보고
async Task MyMethodAsync(IProgress<double> progress = null) { bool done = false; double percentComplete = 0; while (!done) { ... progress?.Report(percentComplete); } } async Task CallMyMethodAsync() { var progress = new Progress<double>(); progress.ProgressChanged += (sender, args) => { ... }; await MyMethodAsync(progress); }
모든 작업의 완료 대기
Task task1 = Task.Delay(TimeSpan.FromSeconds(1)); Task task2 = Task.Delay(TimeSpan.FromSeconds(2)); Task task3 = Task.Delay(TimeSpan.FromSeconds(1)); await Task.WhenAll(task1, task2, task3); Task<int> task1 = Task.FromResult(1); Task<int> task2 = Task.FromResult(3); Task<int> task3 = Task.FromResult(5); int[] results = await Task.WhenAll(task1, task2, task3); // results = [1, 3, 5]
Example:
async Task<string> DownloadAllAsync(HttpClient client, IEnumerable<string> urls) { var downloads = urls.Select(url => client.GetStringAsync(url)); // 아직 실제로 시작한 작업은 없다. // 동시에 모든 URL에서 다운로드 시작 Task<string>[] downloadTasks = downloads.ToArray(); // 모든 다운로드 완료를 비동기적으로 대기 string[] htmlPages = await Task.WhenAll(downloadTasks); return string.Concat(htmlPages); }
작업 중 하나가 예외를 일으키면 Task.WhenAll은 작업과 함께 해당 예외를 반환하며 실패한다.
여러 작업이 예외를 일으키면 모든 예외를 Task.WhenAll이 반환하는 Task에 넣는다.
하지만 작업이 대기 상태면 예외 중 하나만 일으킨다.
async Task ThrowNotImplementedExceptionAsync() { throw new NotImplementedException(); } async Task ThrowInvalidOperationExceptionAsync() { throw new InvalidOperationException(); } async Task ObserveOneExceptionAsync() { var task1 = ThrowNotImplementedExceptionAsync(); var task2 = ThrowInvalidOperationExceptionAsync(); try { await Task.WhenAll(task1, task2); } catch (Excpeiton ex) { // ex는 NotImplementedException or InvalidOperationException Trace.WriteLine(ex); } } async Task ObserveAllExcpetionAsync() { var task1 = ThrowNotImplementedExceptionAsync(); var task2 = ThrowInvalidOperationExceptionAsync(); Task allTasks = Task.WhenAll(task1, task2); try { await allTasks; } catch { AggregateException allExceptions = allTasks.Excpetion; ... } }
작업이 완료될 때마다 처리
async Task<int> DelayAndReturnAsync(int value) { await Task.Delay(TimeSpan.FromSeconds(value)); return value; } async Task AwaitAndProcessAsync(Task<int> task) { int rv = await task; Trace.WriteLine(rv); } async Task ProcessTasksAsync(int flag) { Task<int> taskA = DelayAndReturnAsync(2); Task<int> taskB = DelayAndReturnAsync(3); Task<int> taskC = DelayAndReturnAsync(1); var tasks = new Task[] { taskA, taskB, taskC }; Task[] processingTasks; if (flag == 1) { IEnumerable<Task> taskQuery = from t in tasks select AwaitAndProcessAsync(t); processingTasks = taskQuery.ToArray(); await Task.WhenAll(processingTasks); } else if (flag == 2) { processingTasks = tasks.Select(async t => { var rv = await t; Trace.WriteLine(rv); }).ToArray(); await Task.WhenAll(processingTasks); } else if (flag == 3) { foreach (Task<int> task in tasks.OrderByCompletion()) { int rv = await task; Trace.WriteLine(rv); } } }
async void 메서드의 예외 처리
sealed class MyAsyncCommand : ICommand { async void ICommand.Execute(object parameter) { await Execute(parameter); } public async Task Execute(object parameter) { ; // 비동기 작업 구현 } ; // CanExecute 등 구현 }
ValueTask 생성/사용
- 반환할 수 있는 동기적 결과가 있고 비동기 동작이 드문 상황에서 반환 형식으로 사용
- 프로파일링을 통해 애플리케이션의 성능 향상을 확인할 수 있을 때만 고려해야 함
- ValueTask를 반환하는 DisposeAsync 메서드가 있는 IAsyncDisposable을 구현할 때
private Task<int> SlowMethodAsync(); public ValueTask<int> MethodAsync() { if (CanBehaveSynchronously) return new ValueTask<int>(1); return new ValueTask<int>(SlowMethodAsync()); } async Task ConsumingMethodAsync() { ValueTask<int> valueTask = MethodAsync(); ; // 기타 동시성 작업 int value = await valueTask; ; }
ValueTask는 딱 한 번만 대기할 수 있다.
더 복잡한 작업을 하려면 AsTask를 호출해서 ValueTask<T>를 Task<T>로 변환해야 한다.
ValueTask에서 동기적으로 결과를 얻으려면 ValueTask를 완료한 뒤에 한 번만 할 수 있다.
async Task ConsumingTaskAsync() { Task<int> task = MethodAsync().AsTask(); ; // 기타 동시성 작업 int value = await task; // Task<T>는 await로 여러 번 대기해도 완벽하게 안전하다. int anotherValue = await task; } async Task ConsumingTaskAsync() { Task<int> task1 = MethodAsync().AsTask(); Task<int> task2 = MethodAsync().AsTask(); int[] results = await Task.WhenAll(task1, task2); }
Asynchronous Stream
async IAsyncEnumerable<string> GetValuesAsync(HttpClient client) { const int limit = 10; for (int offset = 0; true; offset += limit) { string result = await client.GetStringAsync( $"https://example.com/api/values?offset={offset}&limit={limit}"); string[] valuesOnThisPage = result.Split('\n'); // 현재 페이지의 결과 전달 foreach (string value in valuesOnThisPage) yield return value; // 마지막 페이지면 끝 if (valuesOnThisPage.Length != limit) break; } public async Task ProcessValuesAsync(HttpClient client) { await foreach (string value in GetValuesAsync(client)) { Console.WriteLine(value); } } public async Task ProcessValuesAsync(HttpClient client) { await foreach (string value in GetValuesAsync(client).ConfigureAwait(false)) { await Task.Delay(100).ConfigureAwait(false); // 비동기 작업 Console.WriteLine(value); } }
비동기 스트림과 LINQ 함께 사용
IEnumerable<T>에는 LINQ to Objects가 있고 IObservable<T>에는 LINQ to Events가 있다.
IAsyncEnumerable<T>도 System.Linq.Async NuGet Package를 통해 LINQ 지원
IAsyncEnumerable<int> values = SlowRange().WhereAwait( async value => { // 요소의 포함 여부를 결정할 비동기 작업 수행 await Task.Delay(10); return value % 2 == 0; }) .Where(value => value % 4 == 0); // 결과는 비동기 스트림 await foreach (int result in values) Console.WriteLine(result); // 진행에 따라 속도가 느려지는 시퀀스 생성 async IAsyncEnumerable<int> SlowRange() { for (int i = 0; i < 10; ++i) { await Task.Delay(i * 100); yield return i; } }
Async 접미사는 값을 추출하거나 계산을 수행한 뒤에 비동기 시퀀스가 아닌 비동기 스칼라 값을 반환하는 연산자에만 붙는다.
int count = await SlowRange().CountAsync( value => value % 2 == 0); // 조건자가 비동기적일 땐 AwaitAsync 접미사가 붙는 연산자를 사용 int count = await SlowRange().CountAwaitAsync( async value => { await Task.Delay(10); return value % 2 == 0; });
비동기 스트림 취소
using var cts = new CancellationTokenSource(500); CancellationToken token = ct.Token; await foreach (int result in SlowRange(token)) { Console.WriteLine(result); } // 진행에 따라 속도가 느려지는 시퀀스 생성 async IAsyncEnumerable<int> SlowRange( [EnumeratorCancellation] CancellationToken token = default) { for (int i = 0; i < 10; ++i) { await Task.Delay(i * 100, token); yield return i; } }
비동기 스트림의 열거에 CancellationToken을 추가할 수 있는 WithCancellation 확장 메서드 지원
async Task ConsumeSequence(IAsyncEnumerable<int> items) { using var cts = new CancellationTokenSource(500); CancellationToken token = cts.Token; await foreach (int result in items.WithCancellation(token)) { Console.WriteLine(result); } } await ConsumeSequence(SlowRange());
'.NET > C#' 카테고리의 다른 글
Concurrency - Reactive Programming (0) | 2023.08.16 |
---|---|
Concurrency - Parallel Programming (0) | 2023.08.16 |
Concurrency (동시성) (0) | 2023.08.16 |
Marshaling: 복사 및 고정 (0) | 2021.10.15 |
Array Marshaling (0) | 2021.10.15 |