Understanding Asynchronous Code: A Comparison of Asynchronous Programming in C# and Java

daryashirokova

Darya Shirokova

Posted on June 1, 2023

Understanding Asynchronous Code: A Comparison of Asynchronous Programming in C# and Java

In this blogpost we’ll discuss what the asynchronous code is and explore how its execution can vary in different languages. We will use C# and Java to illustrate different approaches.

Introduction

Let’s imaging we are doing a flat refurbishment. We want to paint the walls and install the new floor, but before we can do that, we need to place an order for the paint and the floor, wait for its delivery and remove all the furniture so that we don’t damage it in the process.

If we are to write a program that executes that for us, we have a choice between asynchronous and synchronous execution.

If we are doing this synchronously, the order will be the following:

  1. Day 0: Place and order for the paint and wait a few days until it is delivered (2 days).
  2. Day 2: Place an order for the wooden floor and wait until it is delivered (20 days - slow delivery).
  3. Day 22: Remove all furniture from your flat (5 days).
  4. Day 27: Paint the walls (5 days).
  5. Day 32: Install the wooden floor (5 days).
  6. Day 37: And you refurbishment is finished :) It took 37 days in total.

This doesn’t sound effective. Surely, we could place an order for the wooden floor without waiting for the delivery of the paint. Or we could start removing the furniture before deliveries have arrived. Or in other words, we could do many operations asynchronously.

The sequence might look like this:

  1. Day 0: Place an order for the paint (the delivery will take 2 days).
  2. Day 0: Place an order for the wooden floor (the delivery will take 20 days).
  3. Day 0: Remove the furniture (5 days).
  4. Day 5: The paint already arrived 3 days ago - let’s paint the walls (5 days).
  5. Day 10: Still waiting for another 10 days until the floors are delivered - we don’t have anything to do right now (10 days).
  6. Day 20: The floor has finally arrived - let’s install it (5 days).
  7. Day 25: Aaand done, it took us 25 days in total.

More formally, in asynchronous execution model (also known as non-blocking), you can initiate the task but don’t necessarily wait for its completion until you actually need the result. In the synchronous model (also known as blocking) you perform all the actions sequentially one-by-one.

That’s a common introduction into synchronous and asynchronous execution models. Programming languages implement this model differently.

We will explore how this example can be implemented in C# and Java and compare their models.

Asynchronous execution models

Asynchronous execution doesn’t necessarily mean each asynchronous operations spawns or uses a separate thread - though multithreading and asynchronicity are closely related, these are separate concepts. For example, messaging queue is one of design patterns to asynchronously process tasks - it can use one or multiple threads (messages will be submitted from a separate thread).

In C#, according to Microsoft documentation, the async / await keywords do not result in the creation of extra threads as asynchronous methods do not execute on their own dedicated threads. Instead, the method runs within the current synchronization context and utilizes the thread's time only when it is actively processing. Synchronization Context sort of represents the execution context in which the code is running and there are some examples where it is single threaded.

If synchronization context is null (as it is in the console app example below), the asynchronous operations will be using a thread pool for execution. If the methods reaches await keyword and yields the execution, its continuation doesn’t necessarily use the same thread.

In Java, there are multiple ways to write asynchronous code. We will only discuss CompletableFuture in this blogpost (note: we won’t cover virtual threads which as of now are available as a preview feature). Java uses separate threads to perform asynchronous tasks and doesn’t switch the context on JVM level. Even if you create a single threaded executor (Executors.newSingleThreadExecutor()), it will still be a second thread (in addition to the main thread) and the main thread will be blocked if asked to await the results of the execution.

Another model is used in Node.js and it utilizes Event Loop for the asynchronous operations. You can read about it in this article.

Let’s now see some examples in C# and Java.

C# example

The code below performs renovation from our intro in the asynchronous paradigm, as explained in the introduction:

internal class Renovation
{
    static int Id => Thread.CurrentThread.ManagedThreadId;
    static async Task BuyPaint()
    {
        Console.WriteLine($"{Id} Placing an order for paint at a local shop - it'll be delivered quickly.");
        await Task.Delay(TimeSpan.FromSeconds(2));
        Console.WriteLine($"{Id} Paint delivered!");
    }

    static async Task BuyWoodenFloor()
    {
        Console.WriteLine($"{Id} Placing an order for wooden floor. The delivery will take over a month.");
        await Task.Delay(TimeSpan.FromSeconds(20));
        Console.WriteLine($"{Id} Wooden floor is finally delivered!");
    }

    static async Task RemoveFurniture()
    {
        Console.WriteLine($"{Id} Starting to remove the furniture... This will take a few days.");
        await Task.Delay(TimeSpan.FromSeconds(5));
        Console.WriteLine($"{Id} Removed all furniture from the room!");
    }

    static async Task PaintingTheWalls()
    {
        Console.WriteLine($"{Id} Now let's paint the walls - should be fairly quick.");
        await Task.Delay(TimeSpan.FromSeconds(5));
        Console.WriteLine($"{Id} Walls are painted!");
    }

    static async Task InstallNewFloor()
    {
        Console.WriteLine($"{Id} Let's install the floor.");
        await Task.Delay(TimeSpan.FromSeconds(5));
        Console.WriteLine($"{Id} Floors are installed!");
    }

    static async Task Main(string[] args)
    {
        Console.WriteLine($"Is Background thread? {Thread.CurrentThread.IsBackground}");
        Console.WriteLine($"Is SynchronizationContext null? {System.Threading.SynchronizationContext.Current == null}");

        Task paint = BuyPaint();
        Task woodenFloor = BuyWoodenFloor();
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} Placed all orders! Let's remove all the furniture from the room...");
        await RemoveFurniture();

        await paint;
        await PaintingTheWalls();

        await woodenFloor;
        await InstallNewFloor();

        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} The renovation is done :)");
        Console.WriteLine($"Is Background thread? {Thread.CurrentThread.IsBackground}");
    }
}
Enter fullscreen mode Exit fullscreen mode

And here is the output:

PS C:\Users\darya\VSCode> dotnet run
**Is Background thread?** False
Is SynchronizationContext null? True
1 Placing an order for paint at a local shop - it'll be delivered quickly.
1 Placing an order for wooden floor. The delivery will take over a month.
1 Placed all orders! Let's remove all the furniture from the room...
1 Starting to remove the furniture... This will take a few days.
5 Paint delivered!
5 Removed all furniture from the room!
5 Now let's paint the walls - should be fairly quick.
5 Walls are painted!
5 Wooden floor is finally delivered!
5 Let's install the floor.
5 Floors are installed!
5 The renovation is done :)
**Is Background thread?** True
Enter fullscreen mode Exit fullscreen mode

There are a few interesting things to note:

  • C# never yields the control of the execution until it encounters await keyword. E.g. it first logs that it placed an order for the painting and yields, then it logs the same for the wooden floor and yields and only then it starts removing the furniture.
  • As mentioned above, they synchronization context is null so the code uses the thread pool. At peak, three methods run in parallel - Main, BuyPaint and BuyWoodenFloor. BuyPaint started on the thread #1, but when it resumes, it uses thread #5 and the execution of the main method continues on this thread afterwards as well.
  • The Main method started on the main thread, but after a few async invocations it logs that is is executed on the background thread in the end.

Java example

Let’s now implement the same example in Java using CompletableFuture:

public class Main {
    static int getId() {
        return (int) Thread.currentThread().threadId();
    }

    static void buyPaint() {
        try {
            System.out.println(getId() + " Placing an order for paint at a local shop - it'll be delivered quickly.");
            TimeUnit.SECONDS.sleep(2);
            System.out.println(getId() + " Paint delivered!");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    static void buyWoodenFloor() {
        try {
            System.out.println(getId() + " Placing an order for wooden floor. The delivery will take over a month.");
            TimeUnit.SECONDS.sleep(20);
            System.out.println(getId() + " Wooden floor is finally delivered!");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    static void removeFurniture() {
        try {
            System.out.println(getId() + " Starting to remove the furniture... This will take a few days.");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(getId() + " Removed all furniture from the room!");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    static void paintingTheWalls() {
        try {
            System.out.println(getId() + " Now let's paint the walls - should be fairly quick.");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(getId() + " Walls are painted!");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    static void installNewFloor() {
        try {
            System.out.println(getId() + " Let's install the floor.");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(getId() + " Floors are installed!");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        Executor executor = Executors.newFixedThreadPool(/* nThreads= */ 3);
        CompletableFuture<Void> paint = CompletableFuture.runAsync(Main::buyPaint, executor);
        CompletableFuture<Void> woodenFloor = CompletableFuture.runAsync(Main::buyWoodenFloor, executor);

        System.out.println(getId() + " Placed all orders! Let's remove all the furniture from the room...");

        removeFurniture();

        paint.join();
        CompletableFuture.runAsync(Main::paintingTheWalls, executor).join();

        woodenFloor.join();
        CompletableFuture.runAsync(Main::installNewFloor, executor).join();

        System.out.println(getId() + " The renovation is done :)");
    }
}
Enter fullscreen mode Exit fullscreen mode

And the output:

23 Placing an order for paint at a local shop - it'll be delivered quickly.
24 Placing an order for wooden floor. The delivery will take over a month.
1 Placed all orders! Let's remove all the furniture from the room...
1 Starting to remove the furniture... This will take a few days.
23 Paint delivered!
1 Removed all furniture from the room!
25 Now let's paint the walls - should be fairly quick.
25 Walls are painted!
24 Wooden floor is finally delivered!
23 Let's install the floor.
23 Floors are installed!
1 The renovation is done :)
Enter fullscreen mode Exit fullscreen mode
  • We see that Java’s CompletableFuture used separate threads from the thread pool we defined for each method.
  • Unlike in C#, the asynchronous methods always fully executes on a separate thread from the beginning to end and don’t yield the execution.
  • The main thread is always occupied even when blocked - the main method always starts and ends on the main thread.

Conclusion

Asynchronous code is a powerful model that can boost responsiveness of the application when used properly. We just had a look into how it might work differently in various languages. This is a huge topic and I don’t attempt to cover all the details in one blogpost, but I hope it provides a demonstration of how seemingly same code can be executed very differently depending on the programming language.


Darya Shirokova is a Software Engineer @ Google

💖 💪 🙅 🚩
daryashirokova
Darya Shirokova

Posted on June 1, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related