Asynchronous Programming 101 Top-down introduction to Asynchronous Programming in C# Ships at sea as seen from a ferry from Redwood City to San Francisco, July 2018 In this blog post I’ll give a top-down introduction of asynchronous programming in C#. This post may be useful for anyone new to C# and for students who have learnt about threads and concurrency. I believe that a top-down approach will be ideal as it’ll first show you async programming is so that you can be interested in the and of it. what how why The code used in this blog is available on my GitHub . here A quick review of the fundamentals Latency & Throughput Latency is a measure of units of time taken to do work. Throughput is measure of the units of work done per unit of time. Concurrency Concurrency means multiple computations are happening at the same time[1]. It is different than parallelism as explained in stackoverflow answer: this Concurrency: A condition that exists when at least two threads are making progress. A more generalized form of parallelism that can include time-slicing as a form of virtual parallelism[2]. Parallelism: A condition that arises when at least two threads are executing simultaneously[2]. Tasks A Task is an abstraction that represents an operation[3]. This operation may or may not be backed by more than one thread and it may or may not be executed concurrently. We can use Tasks instead of working with threads directly so that we don’t need to manually, explicitly implement locking, joining and synchronizing operations for threads and we don’t need to manually create or destroy threads directly either. By default in C# Tasks use threads from a Thread Pool. A Thread Pool is a design pattern that maintains multiple threads such that they can be reused/recycled. We can use this design pattern is to reduce the latency of creating, starting and deleting threads repeatedly. Synchronous versus Asynchronous Operations A synchronous operation will do its work before returning to the caller whereas an asynchronous operation may do some (including all) part of its work after returning to the caller[3]. Purdue University, West Lafayette, IN, I think sometime during winter 2018–2019 An analogy to wrap up these fundamental concepts Let’s consider a laundromat. If you put clothes in two machines at literally the same time, for example one washing machine with one hand and another washing machine with the other hand , then you’re loading the clothes in . But if you load clothes simultaneously in two washing machine such that at a given instance in time you are only loading clothes in one washing machine at a time, then you are loading the clothes . at the same time parallel concurrently The time taken by the washing machine to wash one load of clothes will be the of a washing machine. To reduce time spent waiting for n loads of clothes to be washed, instead of running one machine n times, n machines can be run at the same time: the work done by a machine takes the same amount of time in both the cases but the difference is that the in the latter case amount of work done at in the same amount of time (n loads washed at the same time) is more, i.e., the is more. latency throughput To do all these things maybe you asked the person at the front desk for quarters: you couldn’t do anything at the time, you were at the front of a queue and you gave the person two dollar bills and waited for them to return quarters. This was a operation. When a washing machine is running you still retain control- you can go ahead and load and start running another washing machine while the first one is still running — this makes the running of a washing machine an operation. synchronous asynchronous Async Programming Demo Let’s look at a demo to understand how async functions are defined and called and how async programming impacts program latency by improving throughput. Asynchronous functions in the C# are defined by adding the keyword to the function definition. They can be called like any other functions. Async Functions in C# must return a Task or some type of Task. In the demo program below we will see how these tasks can be initiated, how they can be run synchronously and asynchronously and how the code flow can be stopped to wait for the result of a task by using the keyword. async await Dummy Synchronous and Asynchronous Functions Let’s implement a dummy async function: private async Task<int> longRunningMultiplyBy10(int number){ Console.WriteLine("# Initiated long running op on for the input number=" + number + " from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId + " #"); //100 delays of 100 ms = 10 seconds for (int i = 0; i < 100 ; i++) { await Task.Delay(100); } Console.WriteLine("# Completed long running op on for the input number=" + number + " from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId + " #"); return number * 10;} In this function we use the native asynchronous function by calling it asynchronously using the keyword to wait for a total of 10 seconds and then multiply the input number by 10 and return it. We will now see the difference in performance when it is called synchronously and asynchronously. To do so we will implement functions to take to input numbers, perform the dummy long running operation on each of them and return their sum. Delay await The function to call synchronously is: longRunningMultiplyBy10 private int syncMultiplyBy10AndAdd(int num1, int num2){ Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); int a = longRunningMultiplyBy10(num1).Result; Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); int b = longRunningMultiplyBy10(num2).Result; Console.WriteLine("Executing return from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); return a + b;} Let’s write a function that uses asynchronous programming: private async Task<int> asyncMultiplyBy10AndAddAwaitImmediately(int num1, int num2){ Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); int a = await longRunningMultiplyBy10(num1); Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); int b = await longRunningMultiplyBy10(num2); Console.WriteLine("Executing return from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); return a + b;} We can observe here that we don’t need to on the long running operation until we need to access the Task's result— let’s write a function to reflect this and observe its performance as well. await private async Task<int> asyncMultiplyBy10AndAddAwaitAtEnd(int num1, int num2){ Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); Task<int> a = longRunningMultiplyBy10(num1); Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); Task<int> b = longRunningMultiplyBy10(num2); Console.WriteLine("Executing return from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); return await a + await b;} A tree at Purdue University, West Lafayette, IN, 1st or 2nd week of October, 2018 Helper functions to call dummy functions and record their performance This section is unnecessary and possibly a distraction, skip to next section We will make a call to to call our demo functions and measure their performance as follows: demo private void demo(){ Console.WriteLine("##### Testing synchronous code #####"); demoSyncFunction(syncMultiplyBy10AndAdd); Console.WriteLine("####################################" + Environment.NewLine); Console.WriteLine("##### Testing await immediately #####"); demoAsyncFunction(asyncMultiplyBy10AndAddAwaitImmediately); Console.WriteLine("####################################" + Environment.NewLine); Console.WriteLine(); Console.WriteLine("##### Testing await at end #####"); demoAsyncFunction(asyncMultiplyBy10AndAddAwaitAtEnd); Console.WriteLine("####################################" + Environment.NewLine); Console.WriteLine();} private void demoSyncFunction(Func<int, int, int> asyncDemoFunction){ int num1 = 15; int num2 = 33; int result; Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); result = asyncDemoFunction(num1, num2); stopWatch.Stop(); displayRuntime(stopWatch.Elapsed); } private void demoAsyncFunction(Func<int, int, Task<int>> asyncDemoFunction){ int num1 = 15; int num2 = 33; Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); int result = asyncDemoFunction(num1, num2).Result; stopWatch.Stop(); displayRuntime(stopWatch.Elapsed);} private void displayRuntime(TimeSpan ts){ string elapsedTime = String.Format("{0:00}min:{1:00}s.{2:00}ms", ts.Minutes, ts.Seconds, ts.Milliseconds / 10); Console.WriteLine("Runtime: " + elapsedTime);} A bridge I saw from the window of Amtrak en route to Portland from Seattle or maybe the other around The Results The output is as follows: For the function syncMultiplyBy10AndAdd private int syncMultiplyBy10AndAdd(int num1, int num2){ Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); int a = longRunningMultiplyBy10(num1).Result; Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); int b = longRunningMultiplyBy10(num2).Result; Console.WriteLine("Executing return from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); return a + b;} The output is: ##### Testing synchronous code #####Calling long running Op from Thread 1# Initiated long running op on for the input number=15 from Thread 1 ## Completed long running op on for the input number=15 from Thread 8 #Calling long running Op from Thread 1# Initiated long running op on for the input number=33 from Thread 1 ## Completed long running op on for the input number=33 from Thread 8 #Executing return from Thread 1Runtime: 00min:20s.08ms#################################### We observe that Thread 1 began executing . When it called the execution of was blocked. Thread 1 then began the execution of the long running operation which ran asynchronously. As the function ran asynchronously Thread 1 was released from the task of executing . However, because is synchronous, when completed Thread 1 remained responsible for executing throughout. We note that the function took to execute. syncMultiplyBy10AndAdd longRunningMultiplyBy10 syncMultiplyBy10AndAdd longRunningMultiplyBy10 syncMultiplyBy10AndAdd longRunningMultiplyBy10 syncMultiplyBy10AndAdd 20s.08ms Now when the function was executed: asyncMultiplyBy10AndAddAwaitImmediately private async Task<int> asyncMultiplyBy10AndAddAwaitImmediately(int num1, int num2){ Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); int a = await longRunningMultiplyBy10(num1); Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); int b = await longRunningMultiplyBy10(num2); Console.WriteLine("Executing return from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); return a + b;} The output was ##### Testing await immediately #####Calling long running Op from Thread 1# Initiated long running op on for the input number=15 from Thread 1 ## Completed long running op on for the input number=15 from Thread 7 #Calling long running Op from Thread 7# Initiated long running op on for the input number=33 from Thread 7 ## Completed long running op on for the input number=33 from Thread 8 #Executing return from Thread 8Runtime: 00min:20s.04ms#################################### We observe that Thread 1 began executing . The difference this time is that because the call to was asynchronous Thread 1 was released from the task of executing and another thread from the thread pool, Thread 7, resumed the execution. We note that the function took . This is definitely faster than the synchronous-calls implementation and we can intuitively see how at scale in the real world the improvement in latency will be significant. asyncMultiplyBy10AndAddAwaitImmediately longRunningMultiplyBy10 asyncMultiplyBy10AndAddAwaitImmediately 20s.04ms We can also observe an inefficiency: we ed the completion of the long running operation earlier than when we needed to use its result. So although we used the thread pool to assign the task of executing more efficiently to a thread by using async programming, we didn’t leverage the full power of async programming to allow long running operations (in this case ) to run concurrently with the execution of the caller function (in this case ). await asyncMultiplyBy10AndAddAwaitImmediately longRunningMultiplyBy10 asyncMultiplyBy10AndAddAwaitImmediately When we address this inefficiency and don’t the results of the tasks until we need to use them and then test our function await private async Task<int> asyncMultiplyBy10AndAddAwaitAtEnd(int num1, int num2){ Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); Task<int> a = longRunningMultiplyBy10(num1); Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); Task<int> b = longRunningMultiplyBy10(num2); Console.WriteLine("Executing return from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId); return await a + await b;} we get the following result: ##### Testing await at end #####Calling long running Op from Thread 1# Initiated long running op on for the input number=15 from Thread 1 #Calling long running Op from Thread 1# Initiated long running op on for the input number=33 from Thread 1 #Executing return from Thread 1# Completed long running op on for the input number=33 from Thread 8 ## Completed long running op on for the input number=15 from Thread 6 #Runtime: 00min:10s.04ms#################################### The execution of the long running operation started as soon as it was called. The executions of the caller function and the long running operations continued concurrently. And the runtime of the effectively asynchronously coded function was only : that’s a solid over the synchronous implementation that called 2 long running operations. Since the asynchronously coded function operation calls the 2 long running ops in a way that allows them to run concurrently, the 50% improvement is as expected. 10s.04ms 50% latency improvement Santa Monica on a hottest August day in 2018 (my most favorite picture of the past 12 months) What is Async Programming and How it Works As we have now observed, async programming allows us to effectively utilize threads to run tasks concurrently. When an async function is called the control is returned to the caller function immediately (as soon as the synchronous part of the asynchronous function is completed) and the execution of the caller and the called functions continues concurrently on separate threads until the caller function must the completion of the called function. This helps improve the overall program latency (the total time the program takes to run) by increasing the throughput (the work done per unit of time) by effectively utilizing the thread pool to assign instances of tasks to different threads that run concurrently. await References [1] [2] Defining Multithreading Terms [3] Chapter 14 Concurrency & Asynchrony of https://web.mit.edu/6.005/www/fa14/classes/17-concurrency/ Oracle Multithreaded Programming Guide C# 5.0 in a Nutshell: The Definitive Reference Fifth Edition by Joseph Albahari, Ben Albahari (Author) A shoutout to : his explanation of asynchronous programming and demo async program inspired me to write this blog. Hilton Lange Originally published at deeptanshumalik.com on July 3, 2019.