C# async and actors
Posted
by Alex.Davies
on Simple Talk
See other posts from Simple Talk
or by Alex.Davies
Published on Sun, 04 Mar 2012 20:22:00 GMT
Indexed on
2012/03/18
18:16 UTC
Read the original article
Hit count: 412
If you read my last post about async, you might be wondering what drove me to write such odd code in the first place.
The short answer is that .NET Demon is written using NAct Actors.
Actors are an old idea, which I believe deserve a renaissance under C# 5. The idea is to isolate each stateful object so that only one thread has access to its state at any point in time. That much should be familiar, it's equivalent to traditional lock-based synchronization.
The different part is that actors pass "messages" to each other rather than calling a method and waiting for it to return. By doing that, each thread can only ever be holding one lock. This completely eliminates deadlocks, my least favourite concurrency problem.
Most people who use actors take this quite literally, and there are plenty of frameworks which help you to create message classes and loops which can receive the messages, inspect what type of message they are, and process them accordingly.
But I write C# for a reason. Do I really have to choose between using actors and everything I love about object orientation in C#?
- Type safety
- Interfaces
- Inheritance
- Generics
As it turns out, no. You don't need to choose between messages and method calls. A method call makes a perfectly good message, as long as you don't wait for it to return. This is where asynchonous methods come in.
I have used NAct for a while to wrap my objects in a proxy layer. As long as I followed the rule that methods must always return void, NAct queued up the call for later, and immediately released my thread. When I needed to get information out of other actors, I could use EventHandlers and callbacks (continuation passing style, for any CS geeks reading), and NAct would call me back in my isolated thread without blocking the actor that raised the event.
Using callbacks looks horrible though. To remind you:
m_BuildControl.FilterEnabledForBuilding(
projects,
enabledProjects => m_OutOfDateProjectFinder.FilterNeedsBuilding(
enabledProjects,
newDirtyProjects =>
{
.......
Which is why I'm really happy that NAct now supports async methods. Now, methods are allowed to return Task rather than just void. I can await those methods, and C# 5 will turn the rest of my method into a continuation for me. NAct will run the other method in the other actor's context, but will make sure that when my method resumes, we're back in my context. Neither actor was ever blocked waiting for the other one. Apart from when they were actually busy doing something, they were responsive to concurrent messages from other sources.
To be fair, you could use async methods with lock statements to achieve exactly the same thing, but it's ugly. Here's a realistic example of an object that has a queue of data that gets passed to another object to be processed:
class QueueProcessor
{
private readonly ItemProcessor m_ItemProcessor = ...
private readonly object m_Sync = new object();
private Queue<object> m_DataQueue = ...
private List<object> m_Results = ...
public async Task ProcessOne()
{
object data = null;
lock (m_Sync)
{
data = m_DataQueue.Dequeue();
}
var processedData = await m_ItemProcessor.ProcessData(data);
lock (m_Sync)
{
m_Results.Add(processedData);
}
}
}
We needed to write two lock blocks, one to get the data to process, one to store the result. The worrying part is how easily we could have forgotten one of the locks. Compare that to the version using NAct:
class QueueProcessorActor : IActor
{
private readonly ItemProcessor m_ItemProcessor = ...
private Queue<object> m_DataQueue = ...
private List<object> m_Results = ...
public async Task ProcessOne()
{
// We are an actor, it's always thread-safe to access our private fields
var data = m_DataQueue.Dequeue();
var processedData = await m_ItemProcessor.ProcessData(data);
m_Results.Add(processedData);
}
}
You don't have to explicitly lock anywhere, NAct ensures that your code will only ever run on one thread, because it's an actor.
Either way, async is definitely better than traditional synchronous code. Here's a diagram of what a typical synchronous implementation might do:
The left side shows what is running on the thread that has the lock required to access the QueueProcessor's data. The red section is where that lock is held, but doesn't need to be.
Contrast that with the async version we wrote above:
Here, the lock is released in the middle. The QueueProcessor is free to do something else. Most importantly, even if the ItemProcessor sometimes calls the QueueProcessor, they can never deadlock waiting for each other.
So I thoroughly recommend you use async for all code that has to wait a while for things. And if you find yourself writing lots of lock statements, think about using actors as well. Using actors and async together really takes the misery out of concurrent programming.
© Simple Talk or respective owner