Many years ago, introduced a way to run asynchronous operations that truly changed how we write concurrent code. The async API used to push the frontier on many aspects of concurrent execution. The introduction of , along with a like API, made this beautiful language ( ) 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. C# C# async / await monadic C# In this post, we will explore some of the async API features while comparing them to what modern 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. C# Java Running Tasks The following code shows how to run a simple task in . C# Notice that we are using 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. .Result A popular approach to non-blocking operation in is the use of , one of the most interesting features in this language. C# async / await By using we can specify the next step in the computation ( ) without thinking about callbacks and most importantly, without blocking. The code after the keyword becomes the callback through a series of transformation that the compiler does for us. await .WriteLine await The equivalent (in ) to the latest code will be the following. C# Notice that the inside is the same code after the 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. lambda .ContinueWith await These two options that we just saw show the only ways we have to chain computational stages in , and in most cases, they are just enough. C# In , the previous example looks almost the same. Java Notice that they are basically the same, only changing the API constructs on each of the platforms. One interesting distinction is that in , the function receives the result of the previous computation, and in receives a task instead, and then we have to extract the from it. Java .thenApply C#, .ContinueWith .Result In , we can also use through like the one offered by ( ). Java await libraries Electronic Arts EA Please, notice that is not a reserved keyword in but instead, a function to be called. However, it is used exactly in the same way as in . The library does some bytecode manipulation in order to obtain the same results generated by the compiler. await Java, C# EA C# Chaining Stages When chaining computational stages in , we are limited to the constructs we just saw above, but let’s take a deeper look at them. C# Notice that provides overloaded functions returning different types such as and . In other words, it can be used for chaining stages where the stage returns a new value or where the stage just some operation and returns nothing ( ). .ContinueWith Task<T> Task run side-effecting void In , this is done by using different monadic operations that do not share the same name. The API reduces the number of overloaded functions, and groups them by name based on their functionality. Java Java Let’s look at how the same example is accomplished in Java Apart from the naming changes, this is exactly the same functionality. However, notice and have different meanings and the intentions behind them are clearly marked in their names. That is not the case in , where is the only method used. .thenApply .thenAccept C# .ContinueWith Inner Async Operations Now, let’s look at where falls a little behind. C# Let’s suppose we have something like this. And then, we want to combine these two operations. A natural way to do it will be the following. However, is a which is definitely not the value we want. final Task<Task<string>> The problem is that does not its result. .ContinueWith flatten In order to get this done, we will have to write another function in the following way. 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). , on the other hand, has all kind of suitable functions to be used. Java Notice how is flattening the result from obtaining which is the value we expect. .thenCompose str CompletionStage<String> Improving C#? It is a fact that the API was designed long before others. Even when it has a very simplistic approach, and it makes extended use of , it might need some refinements in order to catch up. C# async / await Luckily for us, has , and implementing this missing functionality is just a matter of understanding all these pieces that need to work together. C# extension methods We are going to add three functions on top of the existing API. , , and . Map FlatMap ForEach is basically the same as but we are going to use this new name since it goes well with what others use. Map .ContinueWith flattens the results of previous tasks, so it is the equivalent to in FlatMap .thenCompose Java. 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. ForEach Notice that we are extending , but we are chaining operations using . Task<T> await Now, we implement . FlatMap This is another extension using a generic implementation of our previous . Notice that returns instead of as in . Then the inner task is flattened using . flatten fn Task<Result> Result Map await Finally, we add . ForEach is used for side effects and operations, doesn’t return any value. is equivalent to in . ForEach void fn ForEach .thenAccept Java Now, we can use this constructs to write the previous example as follows. Conclusions 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# C# extension methods , 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 , 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. Java Scala The point is that we should be able to recognize these faults, and then work in order to correct them with minimum effort. already provides the tools to incorporate new features with simplicity, so let’s use them. C# Happy Coding…
Share Your Thoughts