Connecting...

W1siziisimnvbxbpbgvkx3rozw1lx2fzc2v0cy9zawduawz5lxrly2hub2xvz3kvanbnl2jhbm5lci1kzwzhdwx0lmpwzyjdxq

C# Async API: The Missing Parts by Nicolas A Perez

W1siziisijiwmtkvmdevmjivmtuvmjyvndyvnjy3l3blegvscy1wag90by0ynji0odguanblzyjdlfsiccisinrodw1iiiwiotawedkwmfx1mdazzsjdxq

Explore features of C# async API and how they compare to the offerings of modern Java. Software Engineer, Nicolas A Perez analyses the features by writing small pieces of code with the aim of improving the APIs. 

It should take minimum effort to correct a fault and after reading this article you will be on your way to solving your coding faults. 

 

'Many years ago, C# introduced a way to run asynchronous operations that truly changed how we write concurrent code. The C# async API used to push the frontier on many aspects of concurrent execution. The introduction of 'async / await', along with a monadic like API, made this beautiful language (C#) very desirable when coding multi-threading workloads. However, the time has passed, and other languages continue to update their APIs while .NET has kept unchanged.

In this post, we will explore some of the C# async API features while comparing them to what modern Java offers. This is not intended to criticize any of these features, but instead, to analyze them by writing some small pieces of codes that might actually improve these APIs.

 

Running Tasks

The following code shows how to run a simple task in C#.

1 Task<int> GetId()
2 {
3     return Task.Factory.StartNew(() => new Random().Next());
4 }
5
6 var task = GetId();
7
8 Console.WriteLine(task.Result);

Notice that we are using '.Result' to get the value returned from the task. As you might expect, this operation will block until the value becomes available, in other words, it blocks until the task is completed.

A popular approach to non-blocking operation in C# is the use of 'async / await', one of the most interesting features in this language.

1 int id = await GetId();
2
3 Console.WriteLine(id);

By using 'await' we can specify the next step in the computation ('.WriteLine') without thinking about callbacks and most importantly, without blocking. The code after the 'await' keyword becomes the callback through a series of transformation that the compiler does for us.

The equivalent (in C#) to the latest code will be the following.

1 GetId()
2     .ContinueWith(task => Console.WriteLine(task.Result));

Notice that the lambda inside '.ContinueWith' is the same code after the 'await' in the previous example. In fact, that is what the compiler does when transforming one into the other. In both ways, the computations are executed without any blocking, maximizing the asynchronous execution.

These two options that we just saw show the only ways we have to chain computational stages in C#, and in most cases, they are just enough.

In Java, the previous example looks almost the same.

1 CompletionStage<Integer> getId() {
2     return CompletableFuture.supplyAsync(() -> new Random().nextInt());
3 }
4
5 getId()
6     .thenApply(id -> System.out.println(value));

Notice that they are basically the same, only changing the API constructs on each of the platforms.

One interesting distinction is that in Java, the '.thenApply' function receives the result of the previous computation, and in C#, '.ContinueWith' receives a task instead, and then we have to extract the '.Result' from it.

In Java, we can also use 'await' through libraries like the one offered by Electronic Arts (EA).

1 Integer id = await(getId());
2
3 System.out.println(id);

Please, notice that 'await' is not a reserved keyword in Java, but instead, a function to be called. However, it is used exactly in the same way as in C#. The EA library does some bytecode manipulation in order to obtain the same results generated by the C# compiler.

 

Chaining Stages

When chaining computational stages in C#, we are limited to the constructs we just saw above, but let’s take a deeper look at them.

1 public Task<String> DownloadContentAsync(string url)
2 {
3     return Task.Factory.StartNew(() => DonwloadContent(url));
4 }
5        
6 string DonwloadContent(string url)
7 {
8     var client = new WebClient();
9     
10     return client.DownloadString(url);
11 }
12
13 DownloadContentAsync("www.google.com")
14     .ContinueWith(contentTask =>
15     {
16         Console.WriteLine(contentTask.Result);
17         
18         return contentTask.Result.Length;
19     })
20     .ContinueWith(sizeTask => 
21     { 
22         Console.WriteLine("done with: " + sizeTask.Result); 
23     });

Notice that '.ContinueWith' provides overloaded functions returning different types such as 'Task<T>' and 'Task'. In other words, it can be used for chaining stages where the stage returns a new value or where the stage just run some side-effecting operation and returns nothing (void).

In Java, this is done by using different monadic operations that do not share the same name. The Java API reduces the number of overloaded functions, and groups them by name based on their functionality.

Let’s look at how the same example is accomplished in Java

1 String getContent(String url) {
2         URL contentURL = new URL(url);
3         
4         BufferedReader reader = new BufferedReader(new InputStreamReader(contentURL.openStream()));
5         
6         String content = reader.lines().collect(Collectors.joining());
7         
8         return content;
9     }
10     
11 CompletionStage<String> getContentAsync(String url) {
12     return CompletableFuture.supplyAsync(() -> getContent(url));
13 }
14     
15 getContentAsync("http://google.com")
16     .thenApply(content -> {
17             System.out.println(content));
18             
19             return content.size;
20        }
21     .thenAccept(size -> System.out.println("done with: " + size));

Apart from the naming changes, this is exactly the same functionality. However, notice '.thenApply' and '.thenAccept' have different meanings and the intentions behind them are clearly marked in their names. That is not the case in C#, where '.ContinueWith' is the only method used.

 

Inner Async Operations

Now, let’s look at where C# falls a little behind.

Let’s suppose we have something like this.

1 Task<int> GetId()
2 {
3     return Task.Factory.StartNew(() => new Random().Next());
4 }
5
6 Task<String> str(int value)
7 {
8     return Task.Factory.StartNew(value.ToString);
9 }

And then, we want to combine these two operations. A natural way to do it will be the following.

1 var final = GetId().ContinueWith(idTask => str(idTask.Result));

However, 'final' is a 'Task<Task<string>>' which is definitely not the value we want.

The problem is that '.ContinueWith' does not flatten its result.

In order to get this done, we will have to write another function in the following way.

1 async Task<String> flatten()
2 {
3     int id = await GetId();
4    
5     return await str(id);
6 }

Even when this works, we will not be able to chain operation any longer, breaking the pattern we have been following since the beginning. Also, it is very specific, so we might want to generalize this function somehow (continue reading).

Java, on the other hand, has all kind of suitable functions to be used.

1 CompletionStage<Integer> getId() {
2     return CompletableFuture.supplyAsync(() -> new Random().nextInt());
3 }
4    
5 CompletionStage<String> str(Integer value) {
6     return CompletableFuture.supplyAsync(value::toString);
7 }
8    
9 CompletionStage<String> future = getId().thenCompose(id -> str(id));

Notice how '.thenCompose' is flattening the result from 'str' obtaining 'CompletionStage<String>' which is the value we expect.

 

Improving C#?

It is a fact that the C# API was designed long before others. Even when it has a very simplistic approach, and it makes extended use of 'async / await', it might need some refinements in order to catch up.

Luckily for us, C# has extension methods, and implementing this missing functionality is just a matter of understanding all these pieces that need to work together.

We are going to add three functions on top of the existing API. 'Map','FlatMap', and 'ForEach'.

  • 'Map' is basically the same as '.ContinueWith' but we are going to use this new name since it goes well with what others use.
  • 'FlatMap' flattens the results of previous tasks, so it is the equivalent to '.thenCompose' in Java.
  • 'ForEach' will be used to chain tasks that do not return any values. This is an already existing functionality, but having a separated function for it makes the intentions clear.
1 public static async Task<Result> Map<T, Result>(this Task<T> task, Func<T, Result> fn)
2 {
3     T result = await task;
4    
5     return fn(result);
6 }

Notice that we are extending 'Task<T>', but we are chaining operations using 'await'.

Now, we implement 'FlatMap'.

1 public static async Task<Result> FlatMap<T, Result>(this Task<T> task, Func<T, Task<Result>> fn)
2 {
3     T value = await task;
4     
5     return await fn(value);
6 }

This is another extension using a generic implementation of our previous 'flatten'. Notice that 'fn' returns 'Task<Result>' instead of 'Result' as in 'Map'. Then the inner task is flattened using 'await'.

Finally, we add 'ForEach'.

1 public static async Task ForEach<T>(this Task<T> task, Action<T> fn)
2 {
3     T result = await task;
4            
5     fn(result);
6 }

'ForEach' is used for side effects and void operations, 'fn' doesn’t return any value. 'ForEach' is equivalent to '.thenAccept' in Java.

Now, we can use this constructs to write the previous example as follows.

1 GetId()
2     .FlatMap(id => str(id))
3     .Map(strValue => "hello " + strValue)
4     .ForEach(txt => Console.WriteLine(txt));

 

Conclusions

C# async API is powerful and simple enough to survive for almost 11 years without major changes. However, there are some gaps that can be filled with simplicity and without modification of existing constructs by using C# extension methods.

Java, on the other hand, can be overloaded with so many different ways to do the same, but its API covers all kind of use cases. A balanced approach is probably found in languages like Scala, where most constructs, such as the one we have seen, are used throughout the entire language in order to maintain some standards across different APIs.

The point is that we should be able to recognize these faults, and then work in order to correct them with minimum effort. C# already provides the tools to incorporate new features with simplicity, so let’s use them.

Happy Coding…'

 

This article was written by Nicolas A Perez and originally posted on Medium.