Yielding with Asynchronous Methods

It’s been two weeks since we released the CTP on DevLabs. Thousands of people have downloaded it, tried it out, and many have sent us their feedback – thanks for all your input! In the coming weeks we will be releasing a new build that will contain some bug fixes and a few suggested improvements.

One of the features that gets a lot of attention is asynchronous methods. This is what allows us to have thousands and thousands of concurrently running agents without a significant memory footprint. This is what allows us to build responsive IO-heavy applications. It is also a good way to explain how cooperative multitasking works in Axum.

Let’s start with a sample application that I was writing the other day for some other blog post:

 agent MainAgent : channel Microsoft.Axum.Application
{
    public MainAgent()
    {
        var pt = new OrderedInteractionPoint<int>();
        
        // Set up a dataflow network
        pt ==> MultiplyByTwo ==> Print;
        
        // Send some numbers to the network
        for(int i=0; i<5; i++) pt <-- i;
        
        PrimaryChannel::Done <-- Signal.Value;
    }
    
    int MultiplyByTwo(int n)
    {
        return n*2;
    }
    
    void Print(int n)
    {
        Console.WriteLine(n);
    }        
}

Before you copy it to Visual Studio, compile and run it – quick, what should be the output of the program?

I was trying to demonstrate that the elements of the dataflow network come out of it in the same order as they were put in. The program was supposed to send five numbers through the pipeline and get back the resulting numbers in the right order.

You might be surprised to discover that the program finishes without printing any numbers. Actually, it’s not hard to see why – the agent sends a message to the Done port before the dataflow network gets around to process the numbers. As soon as the runtime receives a message on the Done port, it terminates the application – sayonara!

If you’re like me, you might be tempted to try to “pause” the agent by putting a call to Console.ReadLine before sending a message to the port Done:

 // Send some numbers to the network
for(int i=0; i<5; i++) pt <-- i;

// Not so fast! Wait until the user hits 
// return before continuing
Console.WriteLine("Press Enter to continue...");
Console.ReadLine();

PrimaryChannel::Done <-- Signal.Value;

Maybe you already see the problem but I didn’t. When I compiled and ran the program, it dutifully asked me to press Enter, which I did, after which it again terminated without printing any numbers!

That’s where it gets interesting.

Because MultiplyByTwo and Print are methods defined in the agent, they cannot run in parallel with each other or with the agent constructor. If they could, they would be able to modify an agent field, resulting in a data race.

For the two methods to execute, the constructor needs to explicitly suspend its execution and give other methods a chance to execute. In other words, it has to cooperatively yield.

In Axum, all methods have to acquire certain permissions before they can start executing concurrently. A method that can mutate domain’s state needs that domain’s writer token; a method defined in an agent needs that agent’s writer token. A method defined in an agent that is defined in a domain needs both tokens. Read-only methods (called true functions in Axum) require reader tokens for the corresponding domain and/or agent. Only one writer token exists per agent or domain, while multiple reader tokens are available.

This might seem complex but the implementation is actually quite straightforward and cheap. The tokens are stored in the TLS and don’t need to be passed from method to method, and there aren’t that many situations where the tokens need to change ownership.

One such situation is calling an asynchronous method, such as receive. When the receive is called and the message isn’t immediately available, the method pauses and releases its tokens to whoever is the next in line to acquire them.

For an asynchronous method, that pause is implemented as a return to the caller, and all the way to the thread pool if all the methods in the call stack are asynchronous. The rest of the method is then transformed by the compiler to run as a continuation.

By the way, I think that the term “asynchronous” to describe such methods is somewhat unfortunate – perhaps a better term would be “pausable” or “restartable” – because the ability to pause and restart is what these methods are all about. As if this were not confusing enough, we also picked different keywords for the experimental C# – ­async – and Axum – asynchronous. (We could not use the async keyword in Axum because it was already used for something else)

Both the experimental C# and the Axum compiler recognize the methods defined with the async/asynchronous modifier, as well as the APM methods, and treat them similarly.

Speaking of the APM methods – if you’ve used the .NET StreamReader class before, you must have noticed that it does not have the APM counterparts of the synchronous methods such as Read, ReadLine etc. Reading a text file line by line is not trivial – you have to read blocks of bytes, finding newlines among them and maintaining the current character position in the buffer so that the next invocation could pick up where the last one left off. Try to do it asynchronously and the complexity quickly becomes unmanageable.

The problem is solved trivially using the asynchronous methods – all you need to do is take the implementation of the TextReader, StreamReader, TextWriter and StreamWriter and sprinkle the methods with the async modifier. That’s all it took for us to make the synchronous methods asynchronous! These classes are now available in the Microsoft.Axum.IO namespace.

This is how the asynchronous ReadLine can be implemented using the classes from the Microsoft.Axum.IO namespace:

 using Microsoft.Axum.IO;
public isolated static class AsyncConsole
{
    public async static string ReadLine()
    {
        var reader = 
            new StreamReader(
                System.Console.OpenStandardInput());
        return reader.ReadLine();
    }
}

Now the call site looks like this:

 // Send some numbers to the network
for(int i=0; i<5; i++) pt <-- i;

// Not so fast! Wait until the user hits
// return before continuing
Console.WriteLine("Press Enter to continue...");
AsyncConsole.ReadLine();
        
PrimaryChannel::Done <-- Signal.Value;

Finally, this works correctly and prints out the numbers 0, 2, 4, 6, and 8, as expected.

The next build of the Axum CTP will contain the new Console class – look for it in the Microsoft.Axum.IO namespace.

Artur Laksberg