Claude Code Plugins

Community-maintained marketplace

Feedback

csharp-async-patterns

@icartsh/icartsh_plugin
0
0

Task, ValueTask, async streams, cancellation 등 C# async/await 패턴을 사용할 때 활용합니다. 비동기 C# 코드를 작성할 때 사용합니다.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name csharp-async-patterns
description Task, ValueTask, async streams, cancellation 등 C# async/await 패턴을 사용할 때 활용합니다. 비동기 C# 코드를 작성할 때 사용합니다.
allowed-tools Bash, Read, Write, Edit

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

  1. Async All the Way: .Result나 .Wait()를 사용하여 비동기 코드를 block 하지 마세요.
  2. Use CancellationToken: 오래 실행되는 작업에는 항상 CancellationToken을 받도록 하세요.
  3. ConfigureAwait in Libraries: 라이브러리 코드에서는 ConfigureAwait(false)를 사용하세요.
  4. Avoid Async Void: 이벤트 핸들러용으로만 async void를 사용하세요.
  5. Return Task Directly: 가능하면 await 없이 Task를 직접 반환하세요.
  6. Use ValueTask for Hot Paths: 자주 호출되거나 동기적으로 실행되는 경우가 많은 메서드에는 ValueTask를 고려하세요.
  7. Handle All Exceptions: 비동기 메서드에서는 항상 예외를 처리하세요.
  8. Don't Mix Blocking and Async: 하나의 호출 체인에는 하나의 패러다임만 선택하세요.
  9. Dispose Async Resources: IAsyncDisposable에는 await using을 사용하세요.
  10. Test with Cancellation: 취소가 올바르게 작동하는지 테스트하세요.

Common Pitfalls

  1. Blocking on Async Code: .Result나 .Wait() 사용은 데드락을 유발합니다.
  2. Forgetting ConfigureAwait: 라이브러리에서 성능 문제를 일으킬 수 있습니다.
  3. Async Void Methods: await가 불가능하며 예외를 삼켜버립니다.
  4. Not Handling Cancellation: CancellationToken 파라미터를 무시하는 것.
  5. Over-using Task.Run: 이미 비동기인 코드를 Task.Run으로 감싸지 마세요.
  6. Capturing Context Unnecessarily: 컨텍스트가 필요 없는 상황에서 리소스를 낭비합니다.
  7. Fire and Forget: await 없이 비동기 작업을 시작하는 것.
  8. Mixing Sync and Async: 혼란을 야기하고 잠재적인 데드락을 만듭니다.
  9. Not Using ValueTask Correctly: ValueTask를 여러 번 await 하는 것.
  10. Ignoring Exceptions in Task.WhenAll: 첫 번째 예외만 catch 하는 것.

When to Use

다음을 수행할 때 이 SKILL을 사용합니다:

  • C#에서 비동기 코드 작성
  • I/O 바운드 작업 구현 (데이터베이스, 네트워크, 파일 시스템)
  • 반응형 UI 애플리케이션 구축
  • 확장 가능한 웹 서비스 구축
  • 데이터 스트림 처리
  • 취소 지원(Cancellation support) 구현
  • ValueTask를 통한 비동기 성능 최적화
  • 병렬 비동기 작업 처리
  • 비동기 코드의 데드락 방지
  • ASP.NET Core 비동기 패턴 작업

Resources