csharp-async-patterns
C# Async Patterns
async/await, Task, ValueTask, async streams 및 cancellation 패턴을 사용하여 C# 비동기 프로그래밍을 마스터합니다. 이 SKILL은 반응성이 뛰어나고 확장이 용이한 애플리케이션을 구축하기 위해 C# 8-12의 모던 비동기 패턴을 다룹니다.
Async/Await Fundamentals
async/await 패턴은 동기 코드처럼 보이고 동작하는 비동기 코드를 작성하는 간단한 방법을 제공합니다.
Basic Async Method
public async Task<string> FetchDataAsync(string url)
{
using var client = new HttpClient();
string result = await client.GetStringAsync(url);
return result;
}
// 비동기 메서드 호출
public async Task ProcessAsync()
{
string data = await FetchDataAsync("https://api.example.com/data");
Console.WriteLine(data);
}
Async Method Signature Rules
// ✅ 올바름 - Task 반환
public async Task ProcessDataAsync()
{
await Task.Delay(1000);
}
// ✅ 올바름 - Task<T> 반환
public async Task<int> CalculateAsync()
{
await Task.Delay(1000);
return 42;
}
// ⚠️ 이벤트 핸들러 전용 - void 반환
public async void Button_Click(object sender, EventArgs e)
{
await ProcessDataAsync();
}
// ❌ 잘못됨 - async가 아니지만 Task 반환
public Task WrongAsync()
{
// async를 사용하거나 Task.FromResult를 사용해야 함
return Task.CompletedTask;
}
Task and Task
Task는 비동기 작업을 나타냅니다. Task는 값을 반환하는 작업을 나타냅니다.
Creating Tasks
// CPU 집약적 작업을 위한 Task.Run
public async Task<int> CalculateSumAsync(int[] numbers)
{
return await Task.Run(() => numbers.Sum());
}
// 이미 계산된 값을 위한 Task.FromResult
public Task<string> GetCachedValueAsync(string key)
{
if (_cache.TryGetValue(key, out var value))
{
return Task.FromResult(value);
}
return FetchFromDatabaseAsync(key);
}
// void 비동기 메서드를 위한 Task.CompletedTask
public Task ProcessIfNeededAsync(bool condition)
{
if (!condition)
{
return Task.CompletedTask;
}
return DoActualWorkAsync();
}
Task Composition
public async Task<Result> ProcessOrderAsync(Order order)
{
// 순차적 실행 (Sequential execution)
await ValidateOrderAsync(order);
await ChargePaymentAsync(order);
await ShipOrderAsync(order);
return new Result { Success = true };
}
public async Task<Result> ProcessOrderParallelAsync(Order order)
{
// 병렬 실행 (Parallel execution)
var validationTask = ValidateOrderAsync(order);
var inventoryTask = CheckInventoryAsync(order);
var pricingTask = CalculatePricingAsync(order);
await Task.WhenAll(validationTask, inventoryTask, pricingTask);
return new Result
{
IsValid = await validationTask,
InStock = await inventoryTask,
Price = await pricingTask
};
}
ValueTask and ValueTask
ValueTask는 결과가 동기적으로 사용 가능한 경우가 많을 때 사용하는 성능 최적화 수단입니다.
When to Use ValueTask
public class CachedRepository
{
private readonly Dictionary<int, User> _cache = new();
private readonly IDatabase _database;
// ✅ ValueTask 사용이 적절한 사례 - 캐시에서 동기적으로 반환되는 경우가 많음
public ValueTask<User> GetUserAsync(int id)
{
if (_cache.TryGetValue(id, out var user))
{
return ValueTask.FromResult(user);
}
return new ValueTask<User>(FetchUserFromDatabaseAsync(id));
}
private async Task<User> FetchUserFromDatabaseAsync(int id)
{
var user = await _database.QueryAsync<User>(id);
_cache[id] = user;
return user;
}
}
ValueTask Best Practices
public class BufferedReader
{
private readonly byte[] _buffer = new byte[4096];
private int _position;
private int _length;
// Hot path 최적화를 위한 ValueTask
public async ValueTask<byte> ReadByteAsync()
{
if (_position < _length)
{
// 동기 경로 - 할당 없음 (No allocation)
return _buffer[_position++];
}
// 비동기 경로 - 데이터 추가 읽기
await FillBufferAsync();
return _buffer[_position++];
}
private async Task FillBufferAsync()
{
_length = await _stream.ReadAsync(_buffer);
_position = 0;
}
}
// ⚠️ ValueTask 규칙
public async Task ConsumeValueTaskAsync()
{
var reader = new BufferedReader();
// ✅ 올바름 - 한 번만 await
byte b = await reader.ReadByteAsync();
// ❌ 잘못됨 - ValueTask를 저장하지 마세요
var task = reader.ReadByteAsync();
await task; // 잠재적 이슈 발생 가능
// ❌ 잘못됨 - 여러 번 await 하지 마세요
var vt = reader.ReadByteAsync();
await vt;
await vt; // 절대 하지 마세요
}
Async Void vs Async Task
async void (드물게 발생)와 async Task (거의 항상 사용)를 언제 사용할지 이해합니다.
The Async Void Problem
// ❌ 나쁨 - await 불가, 예외 처리 안 됨
public async void ProcessDataBadAsync()
{
await Task.Delay(1000);
throw new Exception("Unhandled!"); // 앱 크래시 발생
}
// ✅ 좋음 - await 가능, 예외 처리 가능
public async Task ProcessDataGoodAsync()
{
await Task.Delay(1000);
throw new Exception("Handled!"); // catch 가능
}
// 사용 예시
public async Task CallerAsync()
{
try
{
// async void는 await 불가
ProcessDataBadAsync(); // Fire and forget - 위험함
// async Task는 await 가능
await ProcessDataGoodAsync(); // 여기서 예외 catch됨
}
catch (Exception ex)
{
Console.WriteLine($"Caught: {ex.Message}");
}
}
The Only Valid Use of Async Void
// ✅ 이벤트 핸들러 - 유일하게 허용되는 사례
public partial class MainWindow : Window
{
public async void SaveButton_Click(object sender, RoutedEventArgs e)
{
try
{
await SaveDataAsync();
MessageBox.Show("Saved successfully!");
}
catch (Exception ex)
{
MessageBox.Show($"Error: {ex.Message}");
}
}
private async Task SaveDataAsync()
{
await _repository.SaveAsync(_data);
}
}
ConfigureAwait(false)
라이브러리 코드에서 성능을 위해 synchronization context 캡처를 제어합니다.
Understanding ConfigureAwait
// 라이브러리 코드 - ConfigureAwait(false) 사용
public class DataService
{
public async Task<Data> GetDataAsync(int id)
{
// ConfigureAwait(false) - 컨텍스트를 캡처하지 않음
var json = await _httpClient.GetStringAsync($"/api/data/{id}")
.ConfigureAwait(false);
var data = await DeserializeAsync(json)
.ConfigureAwait(false);
return data;
}
}
// UI 코드 - ConfigureAwait(false) 사용 금지
public class ViewModel
{
public async Task LoadDataAsync()
{
var data = await _dataService.GetDataAsync(42);
// 여기서 UI 컨텍스트가 필요함
this.DataProperty = data; // UI 업데이트
}
}
ConfigureAwait Patterns
public class AsyncLibrary
{
// ✅ ConfigureAwait(false)를 사용한 라이브러리 메서드
public async Task<Result> ProcessAsync(string input)
{
var step1 = await Step1Async(input).ConfigureAwait(false);
var step2 = await Step2Async(step1).ConfigureAwait(false);
var step3 = await Step3Async(step2).ConfigureAwait(false);
return step3;
}
// ✅ ASP.NET Core - 어디서나 ConfigureAwait(false) 안전함
[HttpGet]
public async Task<IActionResult> GetData(int id)
{
// ASP.NET Core에는 synchronization context가 없음
var data = await _repository.GetAsync(id).ConfigureAwait(false);
return Ok(data);
}
}
CancellationToken Patterns
오래 실행되는 작업에 대한 적절한 취약점 지원.
Basic Cancellation
public async Task<List<Result>> ProcessItemsAsync(
IEnumerable<Item> items,
CancellationToken cancellationToken = default)
{
var results = new List<Result>();
foreach (var item in items)
{
// 취소 요청 확인
cancellationToken.ThrowIfCancellationRequested();
var result = await ProcessItemAsync(item, cancellationToken);
results.Add(result);
}
return results;
}
// Timeout과 함께 사용
public async Task<List<Result>> ProcessWithTimeoutAsync(IEnumerable<Item> items)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
return await ProcessItemsAsync(items, cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation timed out");
throw;
}
}
Advanced Cancellation Patterns
public class BackgroundProcessor
{
private CancellationTokenSource? _cts;
public async Task StartAsync()
{
_cts = new CancellationTokenSource();
await ProcessLoopAsync(_cts.Token);
}
public void Stop()
{
_cts?.Cancel();
}
private async Task ProcessLoopAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
await ProcessBatchAsync(cancellationToken);
await Task.Delay(1000, cancellationToken);
}
catch (OperationCanceledException)
{
// 취소 시 예상되는 상황
break;
}
}
}
// 연결된 cancellation tokens (Linked cancellation tokens)
public async Task ProcessWithMultipleTokensAsync(
CancellationToken userToken,
CancellationToken systemToken)
{
using var linkedCts = CancellationTokenSource
.CreateLinkedTokenSource(userToken, systemToken);
await DoWorkAsync(linkedCts.Token);
}
}
Async Streams (IAsyncEnumerable)
IAsyncEnumerable를 사용하여 비동기적으로 데이터를 스트리밍합니다 (C# 8+).
Basic Async Streams
public async IAsyncEnumerable<LogEntry> ReadLogsAsync(
string filePath,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await using var stream = File.OpenRead(filePath);
using var reader = new StreamReader(stream);
string? line;
while ((line = await reader.ReadLineAsync(cancellationToken)) != null)
{
if (TryParseLog(line, out var entry))
{
yield return entry;
}
}
}
// 비동기 스트림 소비
public async Task ProcessLogsAsync(string filePath)
{
await foreach (var log in ReadLogsAsync(filePath))
{
Console.WriteLine($"{log.Timestamp}: {log.Message}");
}
}
Advanced Async Stream Patterns
public class DataStreamProcessor
{
// 필터링이 포함된 비동기 스트림
public async IAsyncEnumerable<Event> GetEventsAsync(
DateTime startDate,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
int page = 0;
while (true)
{
var events = await FetchPageAsync(page++, cancellationToken);
if (events.Count == 0)
yield break;
foreach (var evt in events.Where(e => e.Date >= startDate))
{
yield return evt;
}
}
}
// 비동기 스트림에 대한 LINQ 스타일 작업
public async IAsyncEnumerable<TResult> SelectAsync<TSource, TResult>(
IAsyncEnumerable<TSource> source,
Func<TSource, TResult> selector)
{
await foreach (var item in source)
{
yield return selector(item);
}
}
// 비동기 스트림 버퍼링 (Buffering)
public async IAsyncEnumerable<List<T>> BufferAsync<T>(
IAsyncEnumerable<T> source,
int bufferSize)
{
var buffer = new List<T>(bufferSize);
await foreach (var item in source)
{
buffer.Add(item);
if (buffer.Count >= bufferSize)
{
yield return buffer;
buffer = new List<T>(bufferSize);
}
}
if (buffer.Count > 0)
{
yield return buffer;
}
}
}
Parallel Async Operations
여러 비동기 작업을 동시에 실행합니다.
Task.WhenAll and Task.WhenAny
public async Task<Summary> GetDashboardDataAsync()
{
// 모든 작업을 동시에 시작
var userTask = GetUserDataAsync();
var ordersTask = GetOrdersAsync();
var analyticsTask = GetAnalyticsAsync();
// 모두 완료될 때까지 대기
await Task.WhenAll(userTask, ordersTask, analyticsTask);
return new Summary
{
User = await userTask,
Orders = await ordersTask,
Analytics = await analyticsTask
};
}
// 일부 실패 처리 (Partial failures)
public async Task<Results> ProcessWithPartialFailuresAsync()
{
var tasks = new[]
{
ProcessTask1Async(),
ProcessTask2Async(),
ProcessTask3Async()
};
await Task.WhenAll(tasks.Select(async t =>
{
try
{
await t;
}
catch (Exception ex)
{
// 로그를 남기되 throw 하지 않음
Console.WriteLine($"Task failed: {ex.Message}");
}
}));
// 성공한 결과 수집
var results = tasks
.Where(t => t.IsCompletedSuccessfully)
.Select(t => t.Result)
.ToList();
return new Results { Successful = results };
}
Task.WhenAny for Timeouts and Racing
public async Task<T> WithTimeoutAsync<T>(Task<T> task, TimeSpan timeout)
{
var delayTask = Task.Delay(timeout);
var completedTask = await Task.WhenAny(task, delayTask);
if (completedTask == delayTask)
{
throw new TimeoutException("Operation timed out");
}
return await task;
}
// 여러 소스 간 레이싱 (Racing multiple sources)
public async Task<Data> GetFastestDataAsync()
{
var primaryTask = GetFromPrimaryAsync();
var secondaryTask = GetFromSecondaryAsync();
var cacheTask = GetFromCacheAsync();
var completedTask = await Task.WhenAny(primaryTask, secondaryTask, cacheTask);
return await completedTask;
}
// Throttled parallel processing (동시성 제한 병렬 처리)
public async Task<List<Result>> ProcessWithThrottlingAsync(
IEnumerable<Item> items,
int maxConcurrency)
{
var semaphore = new SemaphoreSlim(maxConcurrency);
var tasks = items.Select(async item =>
{
await semaphore.WaitAsync();
try
{
return await ProcessItemAsync(item);
}
finally
{
semaphore.Release();
}
});
return (await Task.WhenAll(tasks)).ToList();
}
Exception Handling in Async Code
비동기 메서드에 대한 적절한 예외 처리 패턴.
Basic Exception Handling
public async Task<Result> ProcessWithErrorHandlingAsync()
{
try
{
var data = await FetchDataAsync();
return await ProcessDataAsync(data);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Network error occurred");
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error occurred");
return Result.Failed(ex.Message);
}
}
// Task.WhenAll과 함께 사용하는 예외 처리
public async Task ProcessMultipleAsync()
{
var tasks = new[] { Task1Async(), Task2Async(), Task3Async() };
try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
// 첫 번째 예외만 throw됨
_logger.LogError(ex, "At least one task failed");
// 모든 예외를 가져오려면:
var exceptions = tasks
.Where(t => t.IsFaulted)
.Select(t => t.Exception)
.ToList();
foreach (var exception in exceptions)
{
_logger.LogError(exception, "Task failed");
}
}
}
AggregateException Handling
public async Task HandleAllExceptionsAsync()
{
var tasks = Enumerable.Range(1, 10)
.Select(i => ProcessItemAsync(i))
.ToArray();
try
{
await Task.WhenAll(tasks);
}
catch
{
// 모든 예외 조사
var aggregateException = new AggregateException(
tasks.Where(t => t.IsFaulted)
.SelectMany(t => t.Exception?.InnerExceptions ?? Array.Empty<Exception>())
);
aggregateException.Handle(ex =>
{
if (ex is HttpRequestException)
{
_logger.LogWarning(ex, "Network error - retrying");
return true; // 처리됨 (Handled)
}
return false; // 다시 throw (Rethrow)
});
}
}
Deadlock Prevention
비동기 코드에서 흔히 발생하는 데드락 상황을 피합니다.
Common Deadlock Patterns
// ❌ DEADLOCK - 비동기 코드에서 blocking 발생
public void DeadlockExample()
{
// UI 또는 ASP.NET 컨텍스트에서 데드락 발생
var result = GetDataAsync().Result;
// 이것 또한 데드락 발생 가능
GetDataAsync().Wait();
}
// ✅ 올바름 - 끝까지 비동기 유지 (async all the way)
public async Task CorrectExample()
{
var result = await GetDataAsync();
}
// ✅ 올바름 - 라이브러리 코드에서 ConfigureAwait(false) 사용
public async Task<Data> LibraryMethodAsync()
{
var data = await FetchAsync().ConfigureAwait(false);
return ProcessData(data);
}
Avoiding Deadlocks
public class DeadlockFreeService
{
// ✅ 끝까지 비동기 유지
public async Task<Result> ProcessAsync()
{
var data = await GetDataAsync();
var processed = await ProcessDataAsync(data);
return processed;
}
// ✅ 부득이하게 block 해야 한다면 Task.Run 사용
public Result ProcessSync()
{
return Task.Run(async () => await ProcessAsync()).GetAwaiter().GetResult();
}
// ✅ 비동기 disposal 사용 (Async disposal)
public async Task UseResourceAsync()
{
await using var resource = new AsyncDisposableResource();
await resource.ProcessAsync();
}
}
Async in ASP.NET Core
ASP.NET Core 애플리케이션의 비동기 코드 모범 사례.
Controller Async Patterns
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repository;
// ✅ Async 액션 메서드
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(
int id,
CancellationToken cancellationToken)
{
var product = await _repository.GetByIdAsync(id, cancellationToken);
if (product == null)
return NotFound();
return Ok(product);
}
[HttpPost]
public async Task<ActionResult<Product>> CreateProduct(
[FromBody] CreateProductRequest request,
CancellationToken cancellationToken)
{
var product = await _repository.CreateAsync(request, cancellationToken);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
// ✅ IAsyncEnumerable을 사용한 응답 스트리밍
[HttpGet("stream")]
public async IAsyncEnumerable<Product> StreamProducts(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var product in _repository.GetAllStreamAsync(cancellationToken))
{
yield return product;
}
}
}
Background Services
public class DataProcessorService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<DataProcessorService> _logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Data processor service starting");
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ProcessDataBatchAsync(stoppingToken);
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
catch (OperationCanceledException)
{
// 중지 시 예상되는 상황
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing data batch");
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
_logger.LogInformation("Data processor service stopped");
}
private async Task ProcessDataBatchAsync(CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();
var repository = scope.ServiceProvider.GetRequiredService<IDataRepository>();
await repository.ProcessBatchAsync(cancellationToken);
}
}
Best Practices
- Async All the Way: .Result나 .Wait()를 사용하여 비동기 코드를 block 하지 마세요.
- Use CancellationToken: 오래 실행되는 작업에는 항상 CancellationToken을 받도록 하세요.
- ConfigureAwait in Libraries: 라이브러리 코드에서는 ConfigureAwait(false)를 사용하세요.
- Avoid Async Void: 이벤트 핸들러용으로만 async void를 사용하세요.
- Return Task Directly: 가능하면 await 없이 Task를 직접 반환하세요.
- Use ValueTask for Hot Paths: 자주 호출되거나 동기적으로 실행되는 경우가 많은 메서드에는 ValueTask를 고려하세요.
- Handle All Exceptions: 비동기 메서드에서는 항상 예외를 처리하세요.
- Don't Mix Blocking and Async: 하나의 호출 체인에는 하나의 패러다임만 선택하세요.
- Dispose Async Resources: IAsyncDisposable에는 await using을 사용하세요.
- Test with Cancellation: 취소가 올바르게 작동하는지 테스트하세요.
Common Pitfalls
- Blocking on Async Code: .Result나 .Wait() 사용은 데드락을 유발합니다.
- Forgetting ConfigureAwait: 라이브러리에서 성능 문제를 일으킬 수 있습니다.
- Async Void Methods: await가 불가능하며 예외를 삼켜버립니다.
- Not Handling Cancellation: CancellationToken 파라미터를 무시하는 것.
- Over-using Task.Run: 이미 비동기인 코드를 Task.Run으로 감싸지 마세요.
- Capturing Context Unnecessarily: 컨텍스트가 필요 없는 상황에서 리소스를 낭비합니다.
- Fire and Forget: await 없이 비동기 작업을 시작하는 것.
- Mixing Sync and Async: 혼란을 야기하고 잠재적인 데드락을 만듭니다.
- Not Using ValueTask Correctly: ValueTask를 여러 번 await 하는 것.
- Ignoring Exceptions in Task.WhenAll: 첫 번째 예외만 catch 하는 것.
When to Use
다음을 수행할 때 이 SKILL을 사용합니다:
- C#에서 비동기 코드 작성
- I/O 바운드 작업 구현 (데이터베이스, 네트워크, 파일 시스템)
- 반응형 UI 애플리케이션 구축
- 확장 가능한 웹 서비스 구축
- 데이터 스트림 처리
- 취소 지원(Cancellation support) 구현
- ValueTask를 통한 비동기 성능 최적화
- 병렬 비동기 작업 처리
- 비동기 코드의 데드락 방지
- ASP.NET Core 비동기 패턴 작업
Resources
More from icartsh/icartsh_plugin
file-organizer
컨텍스트 이해, 중복 파일 찾기, 더 나은 구조 제안 및 클린업 작업 자동화를 통해 컴퓨터의 파일과 폴더를 지능적으로 정리합니다. 인지 부하를 줄이고 수동 작업 없이 디지털 작업 공간을 깔끔하게 유지합니다.
25error-detective
TRACE 프레임워크(Trace, Read, Analyze, Check, Execute)를 사용한 체계적인 디버깅 및 에러 해결입니다. 에러 디버깅, 스택 트레이스(stack traces) 분석, 실패 조사, 근본 원인 분석(root cause analysis) 또는 운영 이슈 트러블슈팅 시 사용합니다.
22code-analyze
.NET 코드에서 정적 분석(Static analysis), 보안 스캔(Security scan) 및 종속성 체크(Dependency check)를 수행합니다. 코드 품질, 보안 감사 또는 취약점 탐지가 포함된 작업에서 사용합니다.
21markdown-pro
세련된 README 파일, 변경 이력(changelog), 기여 가이드(contribution guide) 및 기술 문서를 작성하기 위한 전문가 수준의 Markdown 문서화 SKILL입니다. 사용 사례: (1) 배지와 섹션을 포함한 README 생성, (2) git 히스토리를 이용한 자동 변경 이력 생성, (3) 목차(table of contents) 생성, (4) 기여 가이드라인 작성, (5) 기술 문서 포맷팅, (6) 구문 강조(syntax highlighting)를 포함한 코드 문서화
19coding-conventions
.NET/C#의 코딩 규약, 명명 규칙, 레이아웃, C# 12/13/14의 최신 기능 활용 가이드라인을 정의합니다. C#/.NET 코드 작성 시, 클래스·메서드 명명 시, 코드 포맷팅 시, 또는 사용자가 코딩 규약, 명명 규칙, C# 모범 사례, Primary Constructors, Collection Expressions, field 키워드에 대해 언급했을 때 사용합니다.
14dotnet-build
dotnet CLI를 사용하여 .NET 솔루션/프로젝트를 빌드합니다. 컴파일, 종속성 복원 또는 아티팩트 빌드 작업 시 사용합니다.
13