csharp-async-patterns

async/await, Task, ValueTask, async streams 및 cancellation 패턴을 사용하여 C# 비동기 프로그래밍을 마스터합니다. 이 SKILL은 반응성이 뛰어나고 확장이 용이한 애플리케이션을 구축하기 위해 C# 8-12의 모던 비동기 패턴을 다룹니다.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "csharp-async-patterns" with this command: npx skills add icartsh/icartsh_plugin/icartsh-icartsh-plugin-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&#x3C;User> GetUserAsync(int id)
{
    if (_cache.TryGetValue(id, out var user))
    {
        return ValueTask.FromResult(user);
    }

    return new ValueTask&#x3C;User>(FetchUserFromDatabaseAsync(id));
}

private async Task&#x3C;User> FetchUserFromDatabaseAsync(int id)
{
    var user = await _database.QueryAsync&#x3C;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&#x3C;byte> ReadByteAsync()
{
    if (_position &#x3C; _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&#x3C;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&#x3C;TResult> SelectAsync&#x3C;TSource, TResult>(
    IAsyncEnumerable&#x3C;TSource> source,
    Func&#x3C;TSource, TResult> selector)
{
    await foreach (var item in source)
    {
        yield return selector(item);
    }
}

// 비동기 스트림 버퍼링 (Buffering)
public async IAsyncEnumerable&#x3C;List&#x3C;T>> BufferAsync&#x3C;T>(
    IAsyncEnumerable&#x3C;T> source,
    int bufferSize)
{
    var buffer = new List&#x3C;T>(bufferSize);

    await foreach (var item in source)
    {
        buffer.Add(item);

        if (buffer.Count >= bufferSize)
        {
            yield return buffer;
            buffer = new List&#x3C;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&#x3C;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&#x3C;ActionResult&#x3C;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&#x3C;ActionResult&#x3C;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&#x3C;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&#x3C;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

  • Async/Await Best Practices

  • ConfigureAwait FAQ

  • Async Streams Tutorial

  • ValueTask Overview

  • Task-based Asynchronous Pattern (TAP)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

error-detective

No summary provided by upstream source.

Repository SourceNeeds Review
General

file-organizer

No summary provided by upstream source.

Repository SourceNeeds Review
General

markdown-pro

No summary provided by upstream source.

Repository SourceNeeds Review
General

coding-conventions

No summary provided by upstream source.

Repository SourceNeeds Review