I’m sure people who have been following me and talking to me have noticed how I’ve been looking so much into actor/agent-based concurrency and message-passing – how I’ve been talking about Axum, TPL Dataflow, Erlang, and some of my homebrewn equivalents.
I believe the future of concurrency lies in message-passing. My reason for saying this actually boils down to OOP. In OOP, every method call is a message – both the request and the result.
For example, whenever you do:
var result = calc.Add(40, 2);
You are, in OOP terms, saying:
- Prepare a message to inform calc that I want to call its Add method
- Add the values 40 and 2 to the message
- Send the message to calc
- Wait for calc.Add to do its thing
- Receive the result through a reply message
Something should become obvious to you when I spell it out like this: Why should the thread be stalled waiting for the result to be sent back before going on with other tasks? Surely it could just go on doing something else while calc finishes doing its very heavy computation.
So the point I’m trying to get across is: Whenever you call a method and whenever you get the value of a property, you’re sending a message (or several). This very simple fact has been largely ignored by mainstream developers and frameworks up until recently. Previously, it was all about trying to parallelize algorithms and using locks to synchronize where you just couldn’t parallelize; this obviously doesn’t scale. There’s also asynchrony. For a long time, people have been writing asynchronous code in the form of lambda expressions or anonymous methods, or even worse, as separate methods, passed to some API that starts an asynchronous operation.
You probably saw this one coming: C# 5.0 pretty much solves this entire problem. With the new async and await keywords, we can easily express asynchronous code in its logical form. However, these two keywords alone don’t give us all the power we need. While they allow us to treat every method call as sending a message and retrieving the result, we have no real way to control how execution happens, at what degree of parallelism, in what synchronization context, etc. This is where the new API in .NET vNext, TPL Dataflow, comes in.
TPL Dataflow allows you to set up a bunch of so-called source and target blocks, and then link them together, so that whenever some data is posted to the initial block, the data will automatically be passed through this “dataflow network”. That’s all nice and fancy, but alone, it isn’t much different from any other message-passing framework. C# 5.0 augments TPL Dataflow with the async and await keywords, so that we can write code like this:
agent.Post(Tuple.Create(40, 2)); var result = await agent.Receive();
This looks a bit more awkward than the first code example above, but the advantage here is that we can set up our agents just the way we want to. We can specify the task scheduler they use, how many messages they process per task, what degree of parallelism they utilize, and so on, giving us absolute control over program flow. For more information, see the docs on the TC Labs page.
F# has solved the problem of asynchrony and concurrency differently, through asynchronous workflows and generic agents. For more about that, check Don Syme’s post on async and parallel design patterns in F#.
The cool thing about actors/agents and message-passing is that we get implicit concurrency without a whole lot of effort. We just write our code the way we naturally would, and watch it scale. I say naturally because in the real world, communication happens through messages – why wouldn’t it in programming, too?
Still, we’re not quite there yet when it comes to language support. Erlang/OTP is probably the single language/platform that has the best support for actors, but we have no means of interacting with Erlang/OTP from .NET currently. There’s Axum too, but given that the language effort has stopped, that’s a no-go.
It really is incredible that the most sane approach to concurrency has been ignored for so long. It seems as though, historically, we went from message-passing to parallel algorithms, and then back to message-passing again. How anyone could have believed that parallel algorithms were going to be more scalable is beyond me, but languages like Erlang and Axum have shown us just how important message-passing is.
A collection of links related to this and previous posts: