Navigating the Complexity of C# Async/Await

Ray Hu
2 min readJun 5, 2020

--

About two decades ago, during my tenure at Microsoft, I had the privilege of working on early versions of Internet Information Service (IIS). Back then, IIS operated by maintaining a thread pool, plucking idle threads to handle individual web requests — a paradigm that served its purpose but had inherent limitations.

The performance of IIS was intricately tied to factors like the size of the thread pool, the count of CPU cores, the need for thread respawns, and the associated context switches. These constraints effectively capped the maximum throughput of the web server to a few thousand requests per second.

Then, in 2009, Node.js entered the scene, ushering in the V8 JavaScript engine to the world of web servers. V8 brought with it the concept of asynchronous programming, transforming the way we approached time-consuming tasks. When you invoked a time-consuming operation, you no longer had to worry about blocking a thread — execution seamlessly continued without waiting for the function to return.

C# eventually embraced asynchronous programming in later versions, introducing the async/await paradigm. However, this transition came with its own set of challenges and, dare I say, "mind burdens."

The first burden arises from the need to explicitly tag methods as async and discern which portions of your code are "awaitable." In contrast, if you were wielding JavaScript, V8 automatically determined what could be awaited, sparing you this cognitive overhead.

The second burden arises when you embark on the journey of making those “awaitable” functions actually awaitable. This entails changing return types from regular values to task types, a fundamental shift that cascades through your codebase, potentially triggering a chain reaction of updates.

The third mind burden surfaces when you aim to optimize execution. While awaiting something, you reap the benefit of allowing other code to run on separate worker threads. However, this can become counterproductive for web scenarios, where these threads end up idling, waiting for signals or locks. An optimized approach involves creating multiple task variables using Task.Run and then selectively employing task.Wait to parallelize tasks—an approach that necessitates a deeper understanding of system behavior and adds to your cognitive load.

In a profession already laden with challenges, it makes sense to liberate developers from the burden of wrestling with runtime behavior intricacies. Let them channel their precious time and energy into domain knowledge and business logic — areas where their expertise can truly shine.

This is where dynamic, functional languages and robust runtime engines can come to the aid of developers. I’m not advocating against strongly typed, static languages; their merits shine in large repositories and expansive teams. However, in the quest for efficiency and productivity, we must strike a balance, leveraging the right tools to empower developers to tackle complexity head-on.

--

--

Ray Hu
Ray Hu

Written by Ray Hu

nobody satirist with abnormal knowledge of current affairs

No responses yet