Concurrency in Java vs C#: A Comparative Overview

Tom Rosenfeld
3 min readJun 10, 2023

Introduction

Concurrency is one of the more complex aspects of programming languages. It’s all about handling multiple tasks at the same time in an efficient and reliable manner. Two widely-used programming languages, Java and C#, have made notable strides in this area, embracing structured concurrency as a way to tackle concurrent programming.

In this blog post, we will compare the newly introduced Structured Concurrency feature in Java’s JDK 21 to its C# counterpart, the async/await feature (which was introduced in C# 5.0 in 2012). This should provide a practical insight into how these two languages handle concurrent tasks.

Structured Concurrency in Java

In JDK 21 Java introduces a new feature: Structured Concurrency. This is Java’s response to the complexities and pitfalls commonly associated with concurrent programming. In essence, structured concurrency ensures that concurrently executed tasks have a well-defined and manageable lifetime.

Here is a sample code using the new feature in Java which I’ve taken from this post:

Response handle() throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> user = scope.fork(() -> findUser());
Supplier<Integer> order = scope.fork(() -> fetchOrder());

scope.join() // Join both subtasks
.throwIfFailed(); // ... and propagate errors

// Here, both subtasks have succeeded, so compose their results
return new Response(user.get(), order.get());
}
//...
}

This sample code illustrates a typical use case where you need to fetch a user and an order concurrently. It creates a StructuredTaskScope that helps manage the lifecycle of concurrent tasks. The fork() method allows you to concurrently execute tasks while join() waits for all the forked tasks to complete. Should any task fail, throwIfFailed() ensures that the error is propagated.

Structured concurrency in Java brings robustness and predictability to concurrent programming, but it does require some boilerplate code to set up the scope and handle the tasks’ lifecycle.

Structured Concurrency in C#

C#, on the other hand, has a more concise approach to concurrency, using async/await keywords and the Task class to handle concurrent tasks. Here is an equivalent code snippet in C#:

public async Task<Response> HandleAsync() {
Task<string> userTask = FindUserAsync();
Task<int> orderTask = FetchOrderAsync();
await Task.WhenAll(userTask, orderTask);
return new Response(userTask.Result, orderTask.Result);
}

In C#, you initiate tasks with FindUserAsync() and FetchOrderAsync(). Then, you wait for all the tasks to complete using Task.WhenAll(). Finally, you fetch the results and compose a response.

C#’s approach to structured concurrency is more succinct and relies heavily on its well-established async/await paradigm, resulting in more readable code.

Conclusion

Both Java and C# offer solutions to manage concurrent tasks in a structured way. Java’s Structured Concurrency is much more verbose than C#‘s’ old async/await pattern.

As developers, it’s crucial to recognize the tools at our disposal and utilize them effectively to meet the demands of our projects. The inherent conciseness of C# particularly shines in the realm of concurrent tasks, offering us a highly efficient and readable approach. Through its progressive enhancements, C# continues to empower developers with excellent tools for effectively handling concurrent tasks.

Stay tuned for more updates on programming language features and trends!

--

--