Async / Await vs. PipeTo in Akka.NET Actors
Best Practices and Patterns for Asynchronous Programming with Akka.NET Actors
Many years ago I wrote an article entitled “How to Do Asynchronous I/O with Akka.NET Actors Using PipeTo” - back when await
support was still very much a work in progress. I subsequently wrote a major update on that article in 2016 - talking mostly about the benefits of PipeTo
from a structural point of view.
I recently learned that this article has many of our own users under the impression that Akka.NET actors don’t support await
semantics properly and that PipeTo
is the only “blessed” pathway for working with asynchronous operations inside actors. This is not the case - await
has had first class support inside actors for many years now.
That’s what this post and video cover: async
, await
, and PipeTo
- how do they work differently, when should you consider using one over the other, and what are the “gotchas” that you need to know?
If you want to see the code sample that goes along with this video and blog post, you can find that here: https://github.com/Aaronontheweb/Akka.AsyncAwait
async
and await
inside Akka.NET Actors
First and foremost, async
and await
are natively supported inside Akka.NET actors - typically via the ReceiveAsync<T>
method:
public sealed class AsyncAwaitActor : ReceiveActor
{
public AsyncAwaitActor()
{
Receive<Messages.Batch>(batch =>
{
foreach (var b in Enumerable.Range(0, batch.Size))
{
// preserve the ref of the original sender
Self.Forward(new Messages.Req(b));
}
});
ReceiveAsync<Messages.Req>(async r =>
{
var ack = r.Ack;
await Task.Delay(1);
Sender.Tell(ack);
});
}
}
What ReceiveAsync
signifies is that the receive-handler is going to return a Task
rather than simply being a void
method of some kind - and the actor has infrastructure built-in for suspending processing of any further messages each time an await
is reached inside the ReceiveAsync
method.
When an actor await
s, the following happens:
- The
Mailbox
is suspended, meaning the actor won’t be scheduled to process any additional messages until it’s resumed again; - Once the
await
completes, anActorTaskSchedulerMessage
is sent containing the remainder of theReceiveAsync
method following theawait
- this is a system message so it can be processed ahead of any other messages waiting in the suspendedMailbox
. - The actor will process all
await
s for a given message this way until theReceiveAsync
’sTask
completes and the method has exited, at which point the actor will resume its mailbox and continue with normal message processing.
This is a detailed technical explanation for what’s happening with the actor internally but the bottom line is: await
works the same way inside Akka.NET actors as it does everywhere else in .NET. await
suspends the flow of execution without blocking a thread and allows the re-entrancy when the await
-ed operation completes - the only difference here is that the actors manage the re-entrant portion through their Mailbox
es, since that’s where actor thread-safety / serial message processing originates.
PipeTo
inside Akka.NET Actors
Everything I wrote in “How to Do Asynchronous I/O with Akka.NET Actors Using PipeTo” about PipeTo
’s mechanics is still accurate, but I’ll illustrate how the code works differently from await
-ing inside an actor below:
public sealed class PipeToActor : ReceiveActor
{
public PipeToActor()
{
Receive<Messages.Batch>(batch =>
{
foreach (var b in Enumerable.Range(0, batch.Size))
{
// preserve the ref of the original sender
Self.Forward(new Messages.Req(b));
}
});
Receive<Messages.Req>(r =>
{
var ack = r.Ack;
Task.Delay(1).ContinueWith(tr => ack)
.PipeTo(Sender, Self);
});
}
}
The structure is exactly the same as the AsyncAwaitActor
- with one key difference: the PipeToActor
doesn’t wait for the Task.Delay
to complete prior to processing the next Messages.Req
instance. Instead it kicks off a detatched Task
and uses PipeTo
to deliver the output back into its own mailbox upon completion.
Comparison
In the video accompanying this blog post, “Akka.NET Actors: Async / Await vs PipeTo” you can see me run some live Petabridge.Cmd commands against our code sample for this blog post.
I’m going to use some Jaeger tracing graphs produced by Phobos to help illustrate the difference between await
and PipeTo
from a flow-control perspective.
await
Scenario
pbm
Command: pbm scenario await -b 10
Output
Starting async / await batch of [10]
Received Ack(SeqNo=0)
Received Ack(SeqNo=1)
Received Ack(SeqNo=2)
Received Ack(SeqNo=3)
Received Ack(SeqNo=4)
Received Ack(SeqNo=5)
Received Ack(SeqNo=6)
Received Ack(SeqNo=7)
Received Ack(SeqNo=8)
Received Ack(SeqNo=9)
batch complete
All messages were completed in the original order in which they were sent, which you can see on the Jaeger graph below as well.
Click for a full-sized version of this image
This scenario took roughly 200ms to run.
PipeTo
Scenario
pbm
Command: pbm scenario pipeto -b 10
Output
Starting PipeTo batch of [10]
Received Ack(SeqNo=1)
Received Ack(SeqNo=9)
Received Ack(SeqNo=0)
Received Ack(SeqNo=8)
Received Ack(SeqNo=3)
Received Ack(SeqNo=7)
Received Ack(SeqNo=5)
Received Ack(SeqNo=4)
Received Ack(SeqNo=2)
Received Ack(SeqNo=6)
batch complete
In this case, the messages completed out of order - this because all of the Task
s started by the PipeToActor
ran in parallel, as you can see on the Jaeger graph for this scenario.
Click for a full-sized version of this image
This scenario took roughly 11ms to run - nearly 20 times faster than the await
scenario.
Best Practices
So why the big difference in performance between await
and PipeTo
? Simply stated, it comes down to flow control: await
blocks the flow of execution and PipeTo
does not.
In my original guidance in “How to Do Asynchronous I/O with Akka.NET Actors Using PipeTo” I heavily emphasized PipeTo
largely because of my own recent experience building heavily loaded real-time analytics and marketing automation systems with Akka.NET - we used PipeTo
quite heavily there prior to await
being fully-supported in Akka.NET (back in 2014.) That left users under the impression that await
isn’t properly supported in Akka.NET.
await
has had first class support since mid-2015 and that’s not expected to change any time soon. Plus, over the years of working with hundreds of Akka.NET users my opinions on what should be used when have evolved quite a bit.
I’ll give you the short version:
You Should Use await
When
- You have a sequence of
Task
s that need to be completed in a specific order; i.e. fetch data from web service –> persist to database; it’s much easier to reason about this withawait
than with multipleReceive
handlers andPipeTo
. - You need non-trivial error handling around
async
operations; again,await
makes this a lot easier than writing customContinueWith
statements to do this along withPipeTo
. - Flow control and order of operations is, generally, more contextually important than throughput.
You Should Use PipeTo
When
- Your
Task
s are simple and ordering doesn’t matter; - When you want to still receive user-defined messages mid-
Task
; this one often gets overlooked. How can you stop a long-running operation running in another actor if it can’t process any messages? Either you have toContext.Stop
that actor (sends a system message, which can still be processed) or hold onto a sharedCancellationTokenSource
reference between actors - which breaks encapsulation.
A Note on Performance
As you can see from the Phobos graphs I included above, the PipeTo
sample is significantly faster than the await
sample - but this is only in the context of judging the throughput of a single actor. If you wanted to achieve similar throughput numbers with await
the answer is simple - scale out and run more actors in parallel!
At the end of the day, the decision around await
vs. PipeTo
really comes down to flow control trade-offs. await
is generally a better solution for managing more complex flows, whereas PipeTo
is a more responsive solution that works best on relatively simple solutions.
I use a combination of both in my own code - and we do the same inside Akka.NET’s internals itself. Your mileage may vary!
If you liked this post, you can share it with your followers or follow us on Twitter!
- Read more about:
- Akka.NET
- Case Studies
- Videos
Observe and Monitor Your Akka.NET Applications with Phobos
Did you know that Phobos can automatically instrument your Akka.NET applications with OpenTelemetry?
Click here to learn more.