Community코딩 & 개발github.com

csharp-concurrency-patterns

.NET에서 올바른 동시성 추상화 선택하기 - I/O에는 async/await, 생산자/소비자에는 Channels, 상태 저장 엔터티 관리에는 Akka.NET까지. 꼭 필요한 경우가 아니라면 락과 수동 동기화는 피하세요.

csharp-concurrency-patterns란 무엇인가요?

이 스킬은 .NET 개발자가 동시성 문제를 해결할 때 가장 적합한 도구를 선택할 수 있도록 안내합니다. async/await(기본 선택), Parallel.ForEachAsync(CPU 바운드 병렬 처리), System.Threading.Channels(생산자-소비자 패턴), Reactive Extensions(UI 이벤트 구성), Akka.NET Streams(서버 측 스트림 처리) 및 Akka.NET Actors(상태 머신, 엔터티별 관리) 등 다양한 추상화 계층을 제공합니다. 특히 공유 가변 상태를 피하고, 락 대신 메시지 전달이나 불변성을 활용하는 철학을 강조합니다. 또한 각 도구를 언제 사용하고 언제 피해야 하는지에 대한 명확한 의사 결정 트리와 함께 `lock` 사용을 최후의 수단으로 간주하는 단계적 접근 방식을 제시합니다. 이 스킬은 Claude Code, Cursor, Codex 등 AI 에이전트와의 협업 시 개발자가 복잡한 동시성 문제를 빠르고 정확하게 해결하도록 도와줍니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Aaronontheweb/dotnet-skills/tree/main/skills/csharp-concurrency-patterns

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

.NET 동시성: 올바른 도구 선택하기

이 스킬을 사용해야 하는 경우

다음과 같은 상황에서 이 스킬을 사용하세요:

  • .NET에서 동시 작업을 처리하는 방법을 결정해야 할 때
  • async/await, Channels, Akka.NET 또는 다른 추상화를 사용할지 평가할 때
  • 락, 세마포어 또는 다른 동기화 기본 요소를 사용하고 싶은 유혹을 느낄 때
  • 역압력, 배치 처리, 디바운싱이 필요한 데이터 스트림을 처리해야 할 때
  • 여러 동시 엔터티 간의 상태를 관리해야 할 때

참조 파일

  • advanced-concurrency.md: Akka.NET Streams, Reactive Extensions, Akka.NET Actors(엔터티-퍼-액터, 상태 머신, 클러스터 샤딩) 및 비동기 로컬 함수 패턴

철학

단순하게 시작하고, 필요할 때만 확장하세요.

대부분의 동시성 문제는 async/await으로 해결할 수 있습니다. async/await로 깔끔하게 해결할 수 없는 특정 요구 사항이 있을 때만 더 정교한 도구를 사용하세요.

공유 가변 상태를 피하려고 노력하세요. 동시성을 처리하는 가장 좋은 방법은 동시성 자체를 설계에서 없애는 것입니다. 불변 데이터, 메시지 전달, 격리된 상태(예: 액터)는 버그의 전체 범주를 제거합니다.

락은 예외여야 하며, 규칙이 되어서는 안 됩니다. 공유 가변 상태를 피할 수 없을 때:

  1. 첫 번째 선택: 공유 상태를 피하도록 재설계(불변성, 메시지 전달, 액터 격리)
  2. 두 번째 선택: System.Collections.Concurrent 사용(ConcurrentDictionary 등)
  3. 세 번째 선택: Channel<T>를 사용하여 메시지 전달을 통해 접근 직렬화
  4. 최후의 수단: 간단하고 수명이 짧은 임계 구역에 lock 사용

의사 결정 트리

무엇을 하려고 하시나요?
│
├─► I/O 대기 (HTTP, 데이터베이스, 파일)?
│   └─► async/await 사용
│
├─► 컬렉션을 병렬로 처리 (CPU 바운드)?
│   └─► Parallel.ForEachAsync 사용
│
├─► 생산자/소비자 패턴 (작업 큐)?
│   └─► System.Threading.Channels 사용
│
├─► UI 이벤트 처리 (디바운스, 스로틀, 결합)?
│   └─► Reactive Extensions (Rx) 사용
│
├─► 서버 측 스트림 처리 (역압력, 배치 처리)?
│   └─► Akka.NET Streams 사용
│
├─► 복잡한 전이를 가진 상태 머신?
│   └─► Akka.NET Actors (Become 패턴) 사용
│
├─► 많은 독립 엔터티의 상태 관리?
│   └─► Akka.NET Actors (엔터티-퍼-액터) 사용
│
├─► 여러 비동기 작업 조정?
│   └─► Task.WhenAll / Task.WhenAny 사용
│
└─► 위 중 어느 것도 해당되지 않습니까?
    └─► 스스로에게 물어보세요: "정말로 공유 가변 상태가 필요한가?"
        ├─► 예 → 공유 상태를 피하도록 재설계 고려
        └─► 정말 피할 수 없음 → Channels 또는 Actors를 사용하여 접근 직렬화

레벨 1: async/await (기본 선택)

용도: I/O 바운드 작업, 비차단 대기, 대부분의 일상적인 동시성.

// 간단한 비동기 I/O
public async Task<Order> GetOrderAsync(string orderId, CancellationToken ct)
{
    var order = await _database.GetAsync(orderId, ct);
    var customer = await _customerService.GetAsync(order.CustomerId, ct);
    return order with { Customer = customer };
}

// 병렬 비동기 작업 (독립적인 경우)
public async Task<Dashboard> LoadDashboardAsync(string userId, CancellationToken ct)
{
    var ordersTask = _orderService.GetRecentOrdersAsync(userId, ct);
    var notificationsTask = _notificationService.GetUnreadAsync(userId, ct);
    var statsTask = _statsService.GetUserStatsAsync(userId, ct);

    await Task.WhenAll(ordersTask, notificationsTask, statsTask);

    return new Dashboard(
        Orders: await ordersTask,
        Notifications: await notificationsTask,
        Stats: await statsTask);
}

핵심 원칙: 항상 CancellationToken을 받아들이세요. 라이브러리 코드에서는 ConfigureAwait(false)를 사용하세요. 비동기 코드를 차단하지 마세요.


레벨 2: Parallel.ForEachAsync (CPU 바운드 병렬 처리)

용도: 작업이 CPU 바운드이거나 제어된 동시성이 필요할 때 컬렉션을 병렬로 처리.

public async Task ProcessOrdersAsync(
    IEnumerable<Order> orders,
    CancellationToken ct)
{
    await Parallel.ForEachAsync(
        orders,
        new ParallelOptions
        {
            MaxDegreeOfParallelism = Environment.ProcessorCount,
            CancellationToken = ct
        },
        async (order, token) =>
        {
            await ProcessOrderAsync(order, token);
        });
}

사용하지 말아야 할 경우: 순수 I/O 작업, 순서가 중요한 경우, 역압력이 필요한 경우.


레벨 3: System.Threading.Channels (생산자/소비자)

용도: 작업 큐, 생산자/소비자 패턴, 생산자와 소비자 분리.

public class OrderProcessor
{
    private readonly Channel<Order> _channel;

    public OrderProcessor()
    {
        _channel = Channel.CreateBounded<Order>(new BoundedChannelOptions(100)
        {
            FullMode = BoundedChannelFullMode.Wait
        });
    }

    // 생산자
    public async Task EnqueueOrderAsync(Order order, CancellationToken ct)
    {
        await _channel.Writer.WriteAsync(order, ct);
    }

    // 소비자 (백그라운드 작업으로 실행)
    public async Task ProcessOrdersAsync(CancellationToken ct)
    {
        await foreach (var order in _channel.Reader.ReadAllAsync(ct))
        {
            await ProcessOrderAsync(order, ct);
        }
    }

    public void Complete() => _channel.Writer.Complete();
}

Channels이 적합한 경우: 속도 분리, 역압력이 있는 버퍼링, 작업자에게 팬아웃, 백그라운드 큐.

Channels이 부적합한 경우: 복잡한 스트림 작업(배치 처리, 윈도잉), 엔터티별 상태 저장 처리, 정교한 감독.


레벨 4+: Akka.NET Streams, Reactive Extensions, Actors

스트림 처리, UI 이벤트 구성 또는 상태 저장 엔터티 관리가 필요한 고급 시나리오는 advanced-concurrency.md를 참조하세요.

Akka.NET Streams는 서버 측 배치 처리, 스로틀링 및 역압력에 탁월합니다. Reactive Extensions는 UI 이벤트 구성에 이상적입니다. Akka.NET Actors는 엔터티-퍼-액터 패턴, Become()을 사용한 상태 머신 및 Cluster Sharding을 통한 분산 시스템을 처리합니다.


안티 패턴: 피해야 할 것

비즈니스 로직을 위한 락

// BAD: 공유 상태를 보호하기 위해 락 사용
private readonly object _lock = new();
private Dictionary<string, Order> _orders = new();

public void UpdateOrder(string id, Action<Order> update)
{
    lock (_lock) { if (_orders.TryGetValue(id, out var order)) update(order); }
}

// GOOD: 액터 또는 Channel을 사용하여 접근 직렬화

수동 스레드 관리

// BAD: 수동으로 스레드 생성
var thread = new Thread(() => ProcessOrders());
thread.Start();

// GOOD: Task.Run 또는 더 나은 추상화 사용
_ = Task.Run(() => ProcessOrdersAsync(cancellationToken));

비동기 코드에서 차단

// BAD: 비동기 차단 - 교착 상태 위험!
var result = GetDataAsync().Result;

// GOOD: 모든 곳에서 비동기 사용
var result = await GetDataAsync();

보호 없는 공유 가변 상태

// BAD: 여러 작업이 공유 상태를 변경
var results = new List<Result>();
await Parallel.ForEachAsync(items, async (item, ct) =>
{
    var result = await ProcessAsync(item, ct);
    results.Add(result); // 경쟁 조건!
});

// GOOD: ConcurrentBag 사용
var results = new ConcurrentBag<Result>();

빠른 참조: 언제 어떤 도구를 사용할까?

필요도구예시
I/O 대기async/awaitHTTP 호출, 데이터베이스 쿼리
병렬 CPU 작업Parallel.ForEachAsync이미지 처리, 계산
작업 큐Channel<T>백그라운드 작업 처리
디바운스/스로틀 UI 이벤트Reactive Extensions입력 시 검색, 자동 저장
서버 측 배치 처리/스로틀링Akka.NET Streams이벤트 집계, 속도 제한
상태 머신Akka.NET Actors결제 흐름, 주문 생명 주기
엔터티 상태 관리Akka.NET Actors주문 관리, 사용자 세션
여러 비동기 작업 실행Task.WhenAll대시보드 데이터 로딩
여러 비동기 작업 경쟁Task.WhenAny폴백이 있는 타임아웃
주기적 작업PeriodicTimer상태 확인, 폴링

확장 경로

async/await (여기서 시작)
    │
    ├─► 병렬 처리가 필요하십니까? → Parallel.ForEachAsync
    │
    ├─► 생산자/소비자가 필요하십니까? → Channel<T>
    │
    ├─► UI 이벤트 구성이 필요하십니까? → Reactive Extensions
    │
    ├─► 서버 측 스트림 처리가 필요하십니까? → Akka.NET Streams
    │
    └─► 상태 머신 또는 엔터티 관리가 필요하십니까? → Akka.NET Actors

구체적인 필요가 있을 때만 확장하세요. "만약을 대비해" 액터나 스트림을 사용하지 마세요.

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

akka-hosting-actor-patterns

Patterns for building entity actors with Akka.Hosting - GenericChildPerEntityParent, message extractors, cluster sharding abstraction, akka-reminders, and ITimeProvider. Supports both local testing and clustered production modes.

akka-net-aspire-configuration

Configure Akka.NET with .NET Aspire for local development and production deployments. Covers actor system setup, clustering, persistence, Akka.Management integration, and Aspire orchestration patterns.

akka-net-best-practices

Critical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.

akka-net-management

Akka.Management for cluster bootstrapping, service discovery (Kubernetes, Azure, Config), health checks, and dynamic cluster formation without static seed nodes.

akka-net-testing-patterns

Write unit and integration tests for Akka.NET actors using modern Akka.Hosting.TestKit patterns. Covers dependency injection, TestProbes, persistence testing, and actor interaction verification. Includes guidance on when to use traditional TestKit.

api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

aspire-configuration

Configure Aspire AppHost to emit explicit app config via environment variables; keep app code free of Aspire clients and service discovery.

aspire-integration-testing

Write integration tests using .NET Aspire

aspire-service-defaults

Create a shared ServiceDefaults project for Aspire applications. Centralizes OpenTelemetry, health checks, resilience, and service discovery configuration across all services.

crap-analysis

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

database-performance

Database access patterns for performance. Separate read/write models, avoid N+1 queries, use AsNoTracking, apply row limits, and never do application-side joins. Works with EF Core and Dapper.

dependency-injection-patterns

Organize DI registrations using IServiceCollection extension methods. Group related services into composable Add* methods for clean Program.cs and reusable configuration in tests.

dotnet-devcert-trust

Diagnose and fix .NET HTTPS dev certificate trust issues on Linux. Covers the full certificate lifecycle from generation to system CA bundle inclusion, with distro-specific guidance for Ubuntu, Fedora, Arch, and WSL2.

dotnet-local-tools

Managing local .NET tools with dotnet-tools.json for consistent tooling across development environments and CI/CD pipelines.

dotnet-project-structure

Modern .NET project structure including .slnx solution format, Directory.Build.props, central package management, SourceLink, version management with RELEASE_NOTES.md, and SDK pinning with global.json.

dotnet-slopwatch

Use Slopwatch to detect LLM reward hacking in .NET code changes. Run after every code modification to catch disabled tests, suppressed warnings, empty catch blocks, and other shortcuts that mask real problems.

efcore-patterns

Entity Framework Core best practices including NoTracking by default, query splitting for navigation collections, migration management, dedicated migration services, and common pitfalls to avoid.

ilspy-decompile

Understand implementation details of .NET code by decompiling assemblies. Use when you want to see how a .NET API works internally, inspect NuGet package source, view framework implementation, or understand compiled .NET binaries.

mailpit-integration

Test email sending locally using Mailpit with .NET Aspire. Captures all outgoing emails without sending them. View rendered HTML, inspect headers, and verify delivery in integration tests.

marketplace-publishing

Workflow for publishing skills and agents to the dotnet-skills Claude Code marketplace. Covers adding new content, updating plugin.json, validation, and release tagging.

관련 스킬