
Csharp Concurrency Patterns
- 1.3k installs
- 1.1k repo stars
- Updated July 3, 2026
- aaronontheweb/dotnet-skills
csharp-concurrency-patterns is an agent skill for choosing the right concurrency abstraction in .net - from async/await for i/o to channels for producer/consumer to akka.net for stateful entity management. avoid locks an
About
The csharp-concurrency-patterns skill is designed for choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and. Most concurrency problems can be solved with async/await. Only reach for more sophisticated tools when you have a specific need that async/await can't address cleanly. Invoke when the user asks about csharp concurrency patterns or related SKILL.md workflows.
- Deciding how to handle concurrent operations in .NET.
- Evaluating whether to use async/await, Channels, Akka.NET, or other abstractions.
- Tempted to use locks, semaphores, or other synchronization primitives.
- Need to process streams of data with backpressure, batching, or debouncing.
- Managing state across multiple concurrent entities.
Csharp Concurrency Patterns by the numbers
- 1,299 all-time installs (skills.sh)
- +26 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #887 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
csharp-concurrency-patterns capabilities & compatibility
- Capabilities
- deciding how to handle concurrent operations in · evaluating whether to use async/await, channels, · tempted to use locks, semaphores, or other synch · need to process streams of data with backpressur
What csharp-concurrency-patterns says it does
Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and manual s
Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoi
npx skills add https://github.com/aaronontheweb/dotnet-skills --skill csharp-concurrency-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.3k |
|---|---|
| repo stars | ★ 1.1k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 3, 2026 |
| Repository | aaronontheweb/dotnet-skills ↗ |
How do I choosing the right concurrency abstraction in .net - from async/await for i/o to channels for producer/consumer to akka.net for stateful entity management. avoid locks and?
Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and.
Who is it for?
Developers using csharp concurrency patterns workflows documented in SKILL.md.
Skip if: Skip when the task falls outside csharp-concurrency-patterns scope or needs a different stack.
When should I use this skill?
User asks about csharp concurrency patterns or related SKILL.md workflows.
What you get
Completed csharp-concurrency-patterns workflow with documented commands, files, and expected deliverables.
- C# stream, actor, or reactive concurrency implementations
By the numbers
- Covers 4 concurrency areas: Akka.NET Streams, Reactive Extensions, Akka.NET Actors, and async local functions
Files
.NET Concurrency: Choosing the Right Tool
When to Use This Skill
Use this skill when:
- Deciding how to handle concurrent operations in .NET
- Evaluating whether to use async/await, Channels, Akka.NET, or other abstractions
- Tempted to use locks, semaphores, or other synchronization primitives
- Need to process streams of data with backpressure, batching, or debouncing
- Managing state across multiple concurrent entities
Reference Files
- advanced-concurrency.md: Akka.NET Streams, Reactive Extensions, Akka.NET Actors (entity-per-actor, state machines, cluster sharding), and async local function patterns
The Philosophy
Start simple, escalate only when needed.
Most concurrency problems can be solved with async/await. Only reach for more sophisticated tools when you have a specific need that async/await can't address cleanly.
Try to avoid shared mutable state. The best way to handle concurrency is to design it away. Immutable data, message passing, and isolated state (like actors) eliminate entire categories of bugs.
Locks should be the exception, not the rule. When you can't avoid shared mutable state: 1. First choice: Redesign to avoid it (immutability, message passing, actor isolation) 2. Second choice: Use System.Collections.Concurrent (ConcurrentDictionary, etc.) 3. Third choice: Use Channel<T> to serialize access through message passing 4. Last resort: Use lock for simple, short-lived critical sections
---
Decision Tree
What are you trying to do?
│
├─► Wait for I/O (HTTP, database, file)?
│ └─► Use async/await
│
├─► Process a collection in parallel (CPU-bound)?
│ └─► Use Parallel.ForEachAsync
│
├─► Producer/consumer pattern (work queue)?
│ └─► Use System.Threading.Channels
│
├─► UI event handling (debounce, throttle, combine)?
│ └─► Use Reactive Extensions (Rx)
│
├─► Server-side stream processing (backpressure, batching)?
│ └─► Use Akka.NET Streams
│
├─► State machines with complex transitions?
│ └─► Use Akka.NET Actors (Become pattern)
│
├─► Manage state for many independent entities?
│ └─► Use Akka.NET Actors (entity-per-actor)
│
├─► Coordinate multiple async operations?
│ └─► Use Task.WhenAll / Task.WhenAny
│
└─► None of the above fits?
└─► Ask yourself: "Do I really need shared mutable state?"
├─► Yes → Consider redesigning to avoid it
└─► Truly unavoidable → Use Channels or Actors to serialize access---
Level 1: async/await (Default Choice)
Use for: I/O-bound operations, non-blocking waits, most everyday concurrency.
// Simple async 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 };
}
// Parallel async operations (when independent)
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);
}Key principles: Always accept CancellationToken. Use ConfigureAwait(false) in library code. Don't block on async code.
---
Level 2: Parallel.ForEachAsync (CPU-Bound Parallelism)
Use for: Processing collections in parallel when work is CPU-bound or you need controlled concurrency.
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);
});
}When NOT to use: Pure I/O operations, when order matters, when you need backpressure.
---
Level 3: System.Threading.Channels (Producer/Consumer)
Use for: Work queues, producer/consumer patterns, decoupling producers from consumers.
public class OrderProcessor
{
private readonly Channel<Order> _channel;
public OrderProcessor()
{
_channel = Channel.CreateBounded<Order>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait
});
}
// Producer
public async Task EnqueueOrderAsync(Order order, CancellationToken ct)
{
await _channel.Writer.WriteAsync(order, ct);
}
// Consumer (run as background task)
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 are good for: Decoupling speed, buffering with backpressure, fan-out to workers, background queues.
Channels are NOT good for: Complex stream operations (batching, windowing), stateful per-entity processing, sophisticated supervision.
---
Level 4+: Akka.NET Streams, Reactive Extensions, Actors
For advanced scenarios requiring stream processing, UI event composition, or stateful entity management, see advanced-concurrency.md.
Akka.NET Streams excel at server-side batching, throttling, and backpressure. Reactive Extensions are ideal for UI event composition. Akka.NET Actors handle entity-per-actor patterns, state machines with Become(), and distributed systems via Cluster Sharding.
---
Anti-Patterns: What to Avoid
Locks for Business Logic
// BAD: Using locks to protect shared state
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: Use an actor or Channel to serialize accessManual Thread Management
// BAD: Creating threads manually
var thread = new Thread(() => ProcessOrders());
thread.Start();
// GOOD: Use Task.Run or better abstractions
_ = Task.Run(() => ProcessOrdersAsync(cancellationToken));Blocking in Async Code
// BAD: Blocking on async - deadlock risk!
var result = GetDataAsync().Result;
// GOOD: Async all the way
var result = await GetDataAsync();Shared Mutable State Without Protection
// BAD: Multiple tasks mutating shared state
var results = new List<Result>();
await Parallel.ForEachAsync(items, async (item, ct) =>
{
var result = await ProcessAsync(item, ct);
results.Add(result); // Race condition!
});
// GOOD: Use ConcurrentBag
var results = new ConcurrentBag<Result>();---
Quick Reference: Which Tool When?
| Need | Tool | Example |
|---|---|---|
| Wait for I/O | async/await | HTTP calls, database queries |
| Parallel CPU work | Parallel.ForEachAsync | Image processing, calculations |
| Work queue | Channel<T> | Background job processing |
| UI events with debounce/throttle | Reactive Extensions | Search-as-you-type, auto-save |
| Server-side batching/throttling | Akka.NET Streams | Event aggregation, rate limiting |
| State machines | Akka.NET Actors | Payment flows, order lifecycles |
| Entity state management | Akka.NET Actors | Order management, user sessions |
| Fire multiple async ops | Task.WhenAll | Loading dashboard data |
| Race multiple async ops | Task.WhenAny | Timeout with fallback |
| Periodic work | PeriodicTimer | Health checks, polling |
---
The Escalation Path
async/await (start here)
│
├─► Need parallelism? → Parallel.ForEachAsync
│
├─► Need producer/consumer? → Channel<T>
│
├─► Need UI event composition? → Reactive Extensions
│
├─► Need server-side stream processing? → Akka.NET Streams
│
└─► Need state machines or entity management? → Akka.NET ActorsOnly escalate when you have a concrete need. Don't reach for actors or streams "just in case".
Advanced Concurrency Patterns
Akka.NET Streams, Reactive Extensions, Akka.NET Actors, and async local function patterns for advanced concurrency scenarios.
Contents
- Akka.NET Streams (Complex Stream Processing)
- Reactive Extensions (UI and Event Composition)
- Akka.NET Actors (Stateful Concurrency)
- Prefer Async Local Functions
Akka.NET Streams (Complex Stream Processing)
Use for: Backpressure, batching, debouncing, throttling, merging streams, complex transformations.
using Akka.Streams;
using Akka.Streams.Dsl;
// Batching with timeout
public Source<IReadOnlyList<Event>, NotUsed> BatchEvents(
Source<Event, NotUsed> events)
{
return events
.GroupedWithin(100, TimeSpan.FromSeconds(1)) // Batch up to 100 or 1 second
.Select(batch => batch.ToList() as IReadOnlyList<Event>);
}
// Throttling
public Source<Request, NotUsed> ThrottleRequests(
Source<Request, NotUsed> requests)
{
return requests
.Throttle(10, TimeSpan.FromSeconds(1), 5, ThrottleMode.Shaping);
}
// Parallel processing with ordered results
public Source<ProcessedItem, NotUsed> ProcessWithParallelism(
Source<Item, NotUsed> items)
{
return items
.SelectAsync(4, async item => await ProcessAsync(item)); // 4 parallel
}
// Complex pipeline
public IRunnableGraph<Task<Done>> CreatePipeline(
Source<RawEvent, NotUsed> events,
Sink<ProcessedEvent, Task<Done>> sink)
{
return events
.Where(e => e.IsValid)
.GroupedWithin(50, TimeSpan.FromMilliseconds(500))
.SelectAsync(4, batch => ProcessBatchAsync(batch))
.SelectMany(results => results)
.ToMaterialized(sink, Keep.Right);
}Akka.NET Streams excel at:
- Batching with size AND time limits
- Throttling and rate limiting
- Backpressure that propagates through the entire pipeline
- Merging/splitting streams
- Parallel processing with ordering guarantees
- Error handling with supervision
Reactive Extensions (UI and Event Composition)
Use for: UI event handling, composing event streams, time-based operations in client applications.
Rx shines in UI scenarios where you need to react to user events with debouncing, throttling, or combining multiple event sources.
using System.Reactive.Linq;
// Search-as-you-type with debouncing
public class SearchViewModel
{
public SearchViewModel(ISearchService searchService)
{
SearchResults = SearchText
.Throttle(TimeSpan.FromMilliseconds(300)) // Wait for typing to pause
.DistinctUntilChanged() // Ignore if same text
.Where(text => text.Length >= 3) // Minimum length
.SelectMany(text => searchService.SearchAsync(text).ToObservable())
.ObserveOn(RxApp.MainThreadScheduler); // Back to UI thread
}
public IObservable<string> SearchText { get; }
public IObservable<IList<SearchResult>> SearchResults { get; }
}
// Combining multiple UI events
public IObservable<bool> CanSubmit =>
Observable.CombineLatest(
UsernameValid,
PasswordValid,
EmailValid,
(user, pass, email) => user && pass && email);
// Double-click detection
public IObservable<Point> DoubleClicks =>
MouseClicks
.Buffer(TimeSpan.FromMilliseconds(300))
.Where(clicks => clicks.Count >= 2)
.Select(clicks => clicks.Last());
// Auto-save with debouncing
public IDisposable AutoSave =>
DocumentChanges
.Throttle(TimeSpan.FromSeconds(2))
.Subscribe(async doc => await SaveAsync(doc));Rx is ideal for:
- UI event composition (WPF, WinForms, MAUI, Blazor)
- Search-as-you-type with debouncing
- Combining multiple event sources
- Time-windowed operations in UI
- Drag-and-drop gesture detection
- Real-time data visualization
Rx vs Akka.NET Streams:
| Scenario | Rx | Akka.NET Streams |
|---|---|---|
| UI events | Best choice | Overkill |
| Client-side composition | Best choice | Overkill |
| Server-side pipelines | Works but limited | Better backpressure |
| Distributed processing | Not designed for | Built for this |
| Hot observables | Native support | Requires more setup |
Rule of thumb: Rx for UI/client, Akka.NET Streams for server-side pipelines.
Akka.NET Actors (Stateful Concurrency)
Use for: Managing state for multiple entities, state machines, push-based updates, complex coordination, supervision and fault tolerance.
Entity-Per-Actor Pattern
// Actor per entity - each order has isolated state
public class OrderActor : ReceiveActor
{
private OrderState _state;
public OrderActor(string orderId)
{
_state = new OrderState(orderId);
Receive<AddItem>(msg =>
{
_state = _state.AddItem(msg.Item);
Sender.Tell(new ItemAdded(msg.Item));
});
Receive<Checkout>(msg =>
{
if (_state.CanCheckout)
{
_state = _state.Checkout();
Sender.Tell(new CheckoutSucceeded(_state.Total));
}
else
{
Sender.Tell(new CheckoutFailed("Cart is empty"));
}
});
Receive<GetState>(_ => Sender.Tell(_state));
}
}State Machines with Become
Actors excel at implementing state machines using Become() to switch message handlers:
public class PaymentActor : ReceiveActor
{
private PaymentData _payment;
public PaymentActor(string paymentId)
{
_payment = new PaymentData(paymentId);
Pending();
}
private void Pending()
{
Receive<AuthorizePayment>(msg =>
{
_payment = _payment with { Amount = msg.Amount };
Become(Authorizing);
Self.Tell(new ProcessAuthorization());
});
Receive<CancelPayment>(_ =>
{
Become(Cancelled);
Sender.Tell(new PaymentCancelled(_payment.Id));
});
}
private void Authorizing()
{
Receive<ProcessAuthorization>(async _ =>
{
var result = await _gateway.AuthorizeAsync(_payment);
if (result.Success)
{
_payment = _payment with { AuthCode = result.AuthCode };
Become(Authorized);
}
else
{
Become(Failed);
}
});
Receive<CancelPayment>(_ =>
{
Sender.Tell(new PaymentError("Cannot cancel during authorization"));
});
}
private void Authorized()
{
Receive<CapturePayment>(_ =>
{
Become(Capturing);
Self.Tell(new ProcessCapture());
});
Receive<VoidPayment>(_ =>
{
Become(Voiding);
Self.Tell(new ProcessVoid());
});
}
private void Capturing() { /* ... */ }
private void Voiding() { /* ... */ }
private void Cancelled() { /* Only responds to GetState */ }
private void Failed() { /* Only responds to GetState, Retry */ }
}Distributed Entities with Cluster Sharding
builder.WithShardRegion<OrderActor>(
typeName: "orders",
entityPropsFactory: (_, _, resolver) =>
orderId => Props.Create(() => new OrderActor(orderId)),
messageExtractor: new OrderMessageExtractor(),
shardOptions: new ShardOptions());
var orderRegion = registry.Get<OrderActor>();
orderRegion.Tell(new ShardingEnvelope("order-123", new AddItem(item)));When to Use Akka.NET
Use Akka.NET Actors when you have:
| Scenario | Why Actors? |
|---|---|
| Many entities with independent state | Each entity gets its own actor - no locks |
| State machines | Become() elegantly models state transitions |
| Push-based/reactive updates | Actors naturally support tell-don't-ask |
| Supervision requirements | Parent actors supervise children, auto restart |
| Distributed systems | Cluster Sharding distributes across nodes |
| Long-running workflows | Actors + persistence = durable workflows |
| Real-time systems | Message-driven, non-blocking by design |
| IoT / device management | Each device = one actor, scales to millions |
Don't use Akka.NET when:
| Scenario | Better Alternative |
|---|---|
| Simple work queue | Channel<T> |
| Request/response API | async/await |
| Batch processing | Parallel.ForEachAsync or Akka.NET Streams |
| UI event handling | Reactive Extensions |
| CRUD operations | Standard async services |
The Actor Mindset
Think of actors when your problem looks like:
- "I have thousands of [orders/users/devices] that need independent state"
- "Each entity goes through a lifecycle with different behaviors at each stage"
- "I need to push updates to interested parties when something changes"
- "If processing fails, I want to restart just that entity"
- "This needs to work across multiple servers"
Prefer Async Local Functions
Use async local functions instead of Task.Run(async () => ...) or ContinueWith():
Don't: Anonymous Async Lambda
private void HandleCommand(MyCommand cmd)
{
var self = Self;
_ = Task.Run(async () =>
{
var result = await DoWorkAsync();
return new WorkCompleted(result);
}).PipeTo(self);
}Do: Async Local Function
private void HandleCommand(MyCommand cmd)
{
async Task<WorkCompleted> ExecuteAsync()
{
var result = await DoWorkAsync();
return new WorkCompleted(result);
}
ExecuteAsync().PipeTo(Self);
}Avoid ContinueWith for Sequencing
Don't:
someTask
.ContinueWith(t => ProcessResult(t.Result))
.ContinueWith(t => SendNotification(t.Result));Do:
async Task ProcessAndNotifyAsync()
{
var result = await someTask;
var processed = await ProcessResult(result);
await SendNotification(processed);
}
ProcessAndNotifyAsync();Akka.NET Example
When using PipeTo in actors, async local functions keep the pattern clean:
private void HandleSync(StartSync cmd)
{
async Task<SyncResult> PerformSyncAsync()
{
await using var scope = _scopeFactory.CreateAsyncScope();
var service = scope.ServiceProvider.GetRequiredService<ISyncService>();
var count = await service.SyncAsync(cmd.EntityId);
return new SyncResult(cmd.EntityId, count);
}
PerformSyncAsync().PipeTo(Self);
}| Benefit | Description |
|---|---|
| Readability | Named functions are self-documenting |
| Debugging | Stack traces show meaningful function names |
| Exception handling | Cleaner try/catch without AggregateException |
| Scope clarity | Local functions make captured variables explicit |
| Testability | Easier to extract and unit test the async logic |
Related skills
How it compares
Use csharp-concurrency-patterns for Akka.NET and RX stream graphs; use basic async skills when simple Task-based I/O covers the workload.
FAQ
What does csharp-concurrency-patterns do?
Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and.
When should I use csharp-concurrency-patterns?
User asks about csharp concurrency patterns or related SKILL.md workflows.
Is csharp-concurrency-patterns safe to install?
Review the Security Audits panel on this page before installing in production.