Hello there
My current technology stack: .NET 9, Python, TypeScript, and Azure.
I develop microservices and terraform of different sizes. Sharing my challenges and key learning.
About
The views expressed in this blog are my own and do not reflect my employer's. I am not responsible for any consequences of using the information provided. This blog is for educational purposes only, not for commercial use. Readers should apply their own judgment.
Actor Model vs. Traditional Multi-Threading in .NET 9
Actor Model vs. Traditional Multi-Threading in .NET 9 – Performance
Introduction
Concurrency is one of the biggest challenges in modern .NET applications. Traditional multi-threading (Task-based parallelism) is widely used, but the Actor Model (Akka.NET/Orleans) provides a different approach, using message-passing instead of shared state.
With .NET 9's improvements in thread pooling, async performance, and garbage collection, how do these models compare in terms of throughput, CPU usage, and memory consumption?
In this blog post, will:
✔ Compare Traditional Multi-Threading vs. Actor Model (Akka.NET)
✔ Benchmark 100,000 concurrent requests
✔ Measure CPU, memory, and response times
✔ Determine the best use cases for each model
1. Understanding the Concurrency Models
Traditional Multi-Threading (Task Parallel Library - TPL)
- Uses Threads, Task.Run, Parallel.ForEach to handle concurrency.
- Can lead to race conditions, deadlocks, and high CPU usage if not managed properly.
- Scaling is limited by the number of available threads.
Actor Model (Akka.NET)
- Uses message-passing instead of shared state.
- Avoids race conditions by ensuring each actor processes messages sequentially.
- Scales well across multiple nodes, making it great for distributed systems.
2. Benchmarking Setup in .NET 9
- Traditional Multi-Threading (TPL) handling 100,000 concurrent requests.
- Actor Model using Akka.NET to process the same load.
3. Benchmarking Code
public class RequestMessage
{
public int Id { get; }
public RequestMessage(int id) => Id = id;
}
public class RequestActor : ReceiveActor
{
public RequestActor()
{
Receive<RequestMessage>(msg => HandleRequest(msg));
}
private void HandleRequest(RequestMessage msg)
{
double result = Math.Pow(msg.Id, 3.14) / Math.Sqrt(msg.Id + 1);
}
}
[MemoryDiagnoser]
public class ThreadingVsActorBenchmark
{
private const int RequestCount = 100000;
private readonly ActorSystem _actorSystem;
private readonly IActorRef _requestActor;
public ThreadingVsActorBenchmark()
{
_actorSystem = ActorSystem.Create("RequestSystem");
_requestActor = _actorSystem.ActorOf<RequestActor>();
}
[Benchmark]
public void TraditionalMultiThreading()
{
List<Task> tasks = new();
for (int i = 0; i < RequestCount; i++)
{
tasks.Add(Task.Run(() => ProcessRequest(i)));
}
Task.WaitAll(tasks.ToArray());
}
private void ProcessRequest(int id)
{
double result = Math.Pow(id, 3.14) / Math.Sqrt(id + 1);
}
[Benchmark]
public void ActorModelProcessing()
{
for (int i = 0; i < RequestCount; i++)
{
_requestActor.Tell(new RequestMessage(i));
}
}
}
4. Performance Results
BenchmarkDotNet Results (Windows 11, .NET 9.0.2, Intel i7-1370P)
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4974/23H2/2023Update/SunValley3)
13th Gen Intel Core i7-1370P, 1 CPU, 20 logical and 14 physical cores
.NET SDK 9.0.103
[Host] : .NET 9.0.2 (9.0.225.6610), X64 RyuJIT AVX2 [AttachedDebugger]
DefaultJob : .NET 9.0.2 (9.0.225.6610), X64 RyuJIT AVX2
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
|---|---|---|---|---|---|---|---|
| TraditionalMultiThreading | 24.94 ms | 0.580 ms | 1.608 ms | 1375.0000 | 1343.7500 | 875.0000 | 9.54 MB |
| ActorModelProcessing | 17.18 ms | 0.302 ms | 0.335 ms | 187.5000 | 171.8750 | - | 4.54 MB |
5. Key Observations
✅ Actor Model (Akka.NET) performs ~31% faster than Traditional Multi-Threading under heavy load.
✅ Memory allocation is reduced by ~50% in the Actor Model, making it more efficient.
✅ Garbage collection impact (Gen2) is completely avoided in Actor Model, reducing performance overhead.
❌ Traditional Multi-Threading shows higher Gen1/Gen2 GC pressure, impacting long-term performance.
6. Conclusion
Both concurrency models have their place:
✔ Use Traditional Multi-Threading (TPL) for simpler CPU-bound tasks.
✔ Use the Actor Model (Akka.NET/Orleans) when scalability and fault tolerance are critical.
✔ With .NET 9 optimizations, the Actor Model scales significantly better in high-concurrency environments.