Async and await in C#

When developing a software product or service, system architects need to decide to build or buy the pieces / components needed to develop and deploy the solution. I am stretching this well know strategy to a set of events in my personal life that occurred a month or two ago.

A few years my oldest son and I went to a barber shop once a month. The small place has two chairs. With time only one was being used. At some point the barber decided that he was about to retire. We had to look for an alternate place.

I started going to a hair style salon. With time I noticed that my hair was not even. The gal that styles (the salon does not just cut hair) my hair told me that my hair was probably not growing even. She told me this is quite common with men and women. Since I have my hair cut short and continue to get it cut once a month, I went along with the explanation. That went on for a year or so.

A month or two ago I was visiting my son who lives in Indiana. My wife and I arrived on a Friday and left Sunday. As a coincidence, I changed my appointment that Friday due to the trip, and my son and his two boys had hair appointments that same day. We were hanging together so I went with them to the shop and got my hair cut. The gal used clippers and asked me which sizes I would like for the sides and top. I did not know what she was saying at the time. Apparently there are some standard attachments that are used with clippers to cut hair to different lengths. She suggested a 3 for the sides and a 4 for the top. She would blend the intersection of the two sizes for an even look. The process took about 5 minutes. My hair looked, and more important, grew even during the month.

Back home, the next time I had an appointment, I mentioned to the hair stylist my experience in Indiana. She mentioned that she could not use clippers and the attachments due to salon rules. If she would use them, she could cut my hair is 5 minutes but she had to take 30 minutes total to justify what the salon charges for a haircut. Can you believe that? The salon in Minnesota charges (tip included) about five times as much as the place in Indiana and my hair is not even again. I guess it is the way she cuts hair.

So, build (cut my own hair with the help from my wife) or buy (continue to go to the salon)? It did not take too long to visit Amazon and find clippers with attachments. I can get a nice clipper and attachments for less than what I pay for two haircuts. As I said before, I get my hair cut once a month. My wife can do it in five minutes twice a month. I wonder what other services (e.g., cell phone bill) and products (e.g., restaurants) we are paying way out of proportion for what we get.

OK, enough of hair cut stories and let’s get to the subject at hand. On a previous post JavaScript Promise in this blog, I discussed asynchronous and synchronous methods that can be used to get a set of tasks done. The big issue is that by default Node.js runs with a single thread. If you have one or more I/O tasks that may take a few hundred milliseconds to a few seconds each, it is much better for user response to get them done in parallel. At the time I decided to show a similar approach that is available in C#.

Please note that multiple services and multi-threading still has a place in modern software development. The async and await keywords in C# behave quite similar to their counterparts in JavaScript and C++.

Async and await are the code markers, which marks code positions from where the control should resume after a task completes.

Some APIs that contain async methods are:

HttpClient,

SyndicationClient,

StorageFile,

StreamWriter,

StreamReader,

XmlReader,

MediaCapture,

BitmapEncoder,

BitmapDecoder,

etc.

We will use C# with Visual Studio Enterprise 2017 using .Net Framework 4.6.2. I was chatting a week or two ago with a developer and the issue of not using matching versions on .NET came up. Not using the same version when different teams are developing C# code will create problems during integration. In general, no matter what software is used, it is important to have the same versions for the different code, which may or may not be the latest and greatest release. I guess this is just common sense.

For starters let’s take a look at the following code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TwoMethods
{
    class Program
    {
        // **** constants for methods ****
        public const int Loops1 = 100;
        public const int Loops2 = 25;
     
         static void Main(string[] args)
        {

            // **** start task ****
            Method1();

            // **** start second task ****
            Method2();

            // **** press any key to continue ****
            Console.ReadKey();
        }

        public static async Task Method1()
        {
            await Task.Run(() =>
            {

                // **** loop displaying a message ****
                for (int i = 0; i < Loops1; i++)
                {
                    Console.WriteLine("Method1 <<< i: {0}", i);
                }
            });
        }

        public static void Method2()
        {

            // **** loop displaying a message ****
            for (int i = 0; i < Loops2; i++)
            {
                Console.WriteLine("Method2 <<< i: {0}", i);
            }
        }

    }
}

In this example, the main method starts two tasks and then waits for keyboard input to let them complete and exit. Each task is conducted by a different method. The first method loops for 100 times printing a message, while the second one does the same but only for 25 times.

The console output associated with it follows:

C:\Users\John\Documents\Visual Studio 2017\Projects\TwoMethods\TwoMethods\bin\Debug>twomethods
Method1 <<< i: 0
Method1 <<< i: 1
Method1 <<< i: 2
Method2 <<< i: 0    <==== starts
Method2 <<< i: 1
Method2 <<< i: 2
Method2 <<< i: 3
Method2 <<< i: 4
Method2 <<< i: 5
Method2 <<< i: 6
Method2 <<< i: 7
Method2 <<< i: 8
Method2 <<< i: 9
Method2 <<< i: 10
Method2 <<< i: 11
Method2 <<< i: 12
Method2 <<< i: 13
Method2 <<< i: 14
Method2 <<< i: 15
Method2 <<< i: 16
Method2 <<< i: 17
Method2 <<< i: 18
Method2 <<< i: 19
Method2 <<< i: 20
Method2 <<< i: 21
Method2 <<< i: 22
Method2 <<< i: 23
Method2 <<< i: 24   <==== finishes
Method1 <<< i: 3
Method1 <<< i: 4
Method1 <<< i: 5
Method1 <<< i: 6
Method1 <<< i: 7
Method1 <<< i: 8
Method1 <<< i: 9
Method1 <<< i: 10
Method1 <<< i: 11
Method1 <<< i: 12
Method1 <<< i: 13
Method1 <<< i: 14
Method1 <<< i: 15
Method1 <<< i: 16
Method1 <<< i: 17
Method1 <<< i: 18
Method1 <<< i: 19
Method1 <<< i: 20
Method1 <<< i: 21
Method1 <<< i: 22
Method1 <<< i: 23
Method1 <<< i: 24
Method1 <<< i: 25
Method1 <<< i: 26
Method1 <<< i: 27
Method1 <<< i: 28
Method1 <<< i: 29
Method1 <<< i: 30
Method1 <<< i: 31
Method1 <<< i: 32
Method1 <<< i: 33
Method1 <<< i: 34
Method1 <<< i: 35
Method1 <<< i: 36
Method1 <<< i: 37
Method1 <<< i: 38
Method1 <<< i: 39
Method1 <<< i: 40
Method1 <<< i: 41
Method1 <<< i: 42
Method1 <<< i: 43
Method1 <<< i: 44
Method1 <<< i: 45
Method1 <<< i: 46
Method1 <<< i: 47
Method1 <<< i: 48
Method1 <<< i: 49
Method1 <<< i: 50
Method1 <<< i: 51
Method1 <<< i: 52
Method1 <<< i: 53
Method1 <<< i: 54
Method1 <<< i: 55
Method1 <<< i: 56
Method1 <<< i: 57
Method1 <<< i: 58
Method1 <<< i: 59
Method1 <<< i: 60
Method1 <<< i: 61
Method1 <<< i: 62
Method1 <<< i: 63
Method1 <<< i: 64
Method1 <<< i: 65
Method1 <<< i: 66
Method1 <<< i: 67
Method1 <<< i: 68
Method1 <<< i: 69
Method1 <<< i: 70
Method1 <<< i: 71
Method1 <<< i: 72
Method1 <<< i: 73
Method1 <<< i: 74
Method1 <<< i: 75
Method1 <<< i: 76
Method1 <<< i: 77
Method1 <<< i: 78
Method1 <<< i: 79
Method1 <<< i: 80
Method1 <<< i: 81
Method1 <<< i: 82
Method1 <<< i: 83
Method1 <<< i: 84
Method1 <<< i: 85
Method1 <<< i: 86
Method1 <<< i: 87
Method1 <<< i: 88
Method1 <<< i: 89
Method1 <<< i: 90
Method1 <<< i: 91
Method1 <<< i: 92
Method1 <<< i: 93
Method1 <<< i: 94
Method1 <<< i: 95
Method1 <<< i: 96
Method1 <<< i: 97
Method1 <<< i: 98
Method1 <<< i: 99

As expected, the first method starts displaying and the second one gains control and completes. When done, the first task gains control and finish displaying messages.

In the second example, the main method encapsulates 3 different tasks. After calling the method that dispatches the tasks it also waits for a keystroke on the keyboard to exit.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ThreeMethods
{
    class Program
    {
        static void Main(string[] args)
        {

            // **** calls methods ****
            callMethod();

            // **** press any key to continue ****
            Console.ReadKey();
        }

        public static async void callMethod()
        {

            // **** start first task ****
            Task<int> task = Method1();

            // **** start second task (in parallel) ****
            Method2();

            // **** wait for first task to complete ****
            int count = await task;

            // **** start a third task ****
            Method3(count);
        }

        public static async Task<int> Method1()
        {
            int count = 0;
            await Task.Run(() => {

                // **** loop displaying a message ****
                for (int i = 0; i < 100; i++)
                {
                    Console.WriteLine("Method1 <<< i: {0}", i);
                    count++;
                }
            });
            return count;
        }

        public static void Method2()
        {

            // **** loop displaying a message ****
            for (int i = 0; i < 25; i++)
            {
                Console.WriteLine("Method2 <<< i: {0}", i);
            }
        }

        public static void Method3(int count)
        {
            Console.WriteLine("Method3 <<< count: {0}", count);
        }
    }
}

We start the first method as an asynchronous task. We immediately start a second task. After that, we wait for the first task to complete and get a returned value (the number of loops). To make sure all worked as expected, we then pass to a third task the count for it to display the count and return. The console capture for this program follows:

C:\Users\John\Documents\Visual Studio 2017\Projects\TwoMethods\ThreeMethods\bin\Debug>threemethods
Method1 <<< i: 0
Method1 <<< i: 1
Method2 <<< i: 0    <==== start
Method2 <<< i: 1
Method2 <<< i: 2
Method2 <<< i: 3
Method2 <<< i: 4
Method2 <<< i: 5
Method2 <<< i: 6    <==== releases control
Method1 <<< i: 2
Method1 <<< i: 3
Method1 <<< i: 4
Method1 <<< i: 5
Method1 <<< i: 6
Method1 <<< i: 7
Method1 <<< i: 8
Method2 <<< i: 7    <==== continues
Method2 <<< i: 8
Method2 <<< i: 9
Method2 <<< i: 10
Method2 <<< i: 11
Method2 <<< i: 12
Method2 <<< i: 13
Method2 <<< i: 14   <==== releases control
Method1 <<< i: 9
Method1 <<< i: 10
Method1 <<< i: 11
Method1 <<< i: 12
Method1 <<< i: 13
Method1 <<< i: 14
Method1 <<< i: 15
Method1 <<< i: 16
Method2 <<< i: 15   <==== continues
Method2 <<< i: 16
Method2 <<< i: 17
Method2 <<< i: 18
Method2 <<< i: 19
Method2 <<< i: 20   <==== releases control
Method1 <<< i: 17
Method1 <<< i: 18
Method1 <<< i: 19
Method1 <<< i: 20
Method1 <<< i: 21
Method1 <<< i: 22
Method2 <<< i: 21   <==== continues
Method2 <<< i: 22
Method2 <<< i: 23
Method2 <<< i: 24   <==== finishes
Method1 <<< i: 23
Method1 <<< i: 24
Method1 <<< i: 25
Method1 <<< i: 26
Method1 <<< i: 27
Method1 <<< i: 28
Method1 <<< i: 29
Method1 <<< i: 30
Method1 <<< i: 31
Method1 <<< i: 32
Method1 <<< i: 33
Method1 <<< i: 34
Method1 <<< i: 35
Method1 <<< i: 36
Method1 <<< i: 37
Method1 <<< i: 38
Method1 <<< i: 39
Method1 <<< i: 40
Method1 <<< i: 41
Method1 <<< i: 42
Method1 <<< i: 43
Method1 <<< i: 44
Method1 <<< i: 45
Method1 <<< i: 46
Method1 <<< i: 47
Method1 <<< i: 48
Method1 <<< i: 49
Method1 <<< i: 50
Method1 <<< i: 51
Method1 <<< i: 52
Method1 <<< i: 53
Method1 <<< i: 54
Method1 <<< i: 55
Method1 <<< i: 56
Method1 <<< i: 57
Method1 <<< i: 58
Method1 <<< i: 59
Method1 <<< i: 60
Method1 <<< i: 61
Method1 <<< i: 62
Method1 <<< i: 63
Method1 <<< i: 64
Method1 <<< i: 65
Method1 <<< i: 66
Method1 <<< i: 67
Method1 <<< i: 68
Method1 <<< i: 69
Method1 <<< i: 70
Method1 <<< i: 71
Method1 <<< i: 72
Method1 <<< i: 73
Method1 <<< i: 74
Method1 <<< i: 75
Method1 <<< i: 76
Method1 <<< i: 77
Method1 <<< i: 78
Method1 <<< i: 79
Method1 <<< i: 80
Method1 <<< i: 81
Method1 <<< i: 82
Method1 <<< i: 83
Method1 <<< i: 84
Method1 <<< i: 85
Method1 <<< i: 86
Method1 <<< i: 87
Method1 <<< i: 88
Method1 <<< i: 89
Method1 <<< i: 90
Method1 <<< i: 91
Method1 <<< i: 92
Method1 <<< i: 93
Method1 <<< i: 94
Method1 <<< i: 95
Method1 <<< i: 96
Method1 <<< i: 97
Method1 <<< i: 98
Method1 <<< i: 99
Method3 <<< count: 100  <==== waits for task 1 to run

You can see that the first and second tasks compete for CPU displaying messages to the console. The second task completes first but the main method waits for it to complete to call the third method with the results to display. All seems to work as expected.

In the last and third example, we create, start and wait for an asynchronous task. I decided to add try-catch blocks. The reason is that I tend to use a TDD approach. When I wrote the code, I did not have a file in mind, so I put the name of a non-existing file. AN exception was thrown. I took care of that exception. In practice one should have a catch all case and then try to add the rest of exceptions making sure that the overall software works as expected.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RealTime
{
    class Program
    {
        static void Main(string[] args)
        {

            // **** create a task (manages other tasks) ****
            Task task = new Task(CallMethod);

            // **** start the task ****
            task.Start();

            try
            {
                // **** wait for the task to complete ****
                task.Wait();
            }
            catch (FileNotFoundException ex)
            {
                Console.WriteLine("Main <<< EXCEPTION: " + ex.Message);
            }

            // **** press any key to continue ****
            Console.ReadKey();
        }

        static async void CallMethod()
        {

            const string fileName = "c:\\temp\\1255_store.txt";

            int length = -1;

            // **** create and start a task to read the file ****
            Task<int> task = ReadFile(fileName);

            // **** work on other tasks ****
            Console.WriteLine("CallMethod <<< before work 1");
            Console.WriteLine("CallMethod <<< before work 2");
            Console.WriteLine("CallMethod <<< before work 3");

            // **** wait on the read file task ****
            try
            {
                length = await task;
            }
            catch (FileNotFoundException ex)
            {
                Console.WriteLine("CallMethod <<< EXCEPTION: " + ex.Message);
             }

            // **** display results of the read file task ****
            Console.WriteLine("CallMethod <<< length: " + length);

            // **** continue with other tasks ****
            Console.WriteLine("CallMethod <<< after work 4");
            Console.WriteLine("CallMethod <<< after work 5");
        }

        static async Task<int> ReadFile(string fileName)
        {

            // **** number of bytes ****
            int length = -1;

            // **** inform the user what is going on ****
            Console.WriteLine("ReadFile <<< reading file...");

            // **** read the file and count the number of bytes ****
            try
            {
                using (StreamReader reader = new StreamReader(fileName))
                {

                    // **** asynchronously read the entire file ****
                    string str = await reader.ReadToEndAsync();

                    // **** get the number of bytes read ****
                    length = str.Length;
                }

            }
            catch (FileNotFoundException ex)
            {
                Console.WriteLine("ReadFile <<< EXCEPTION: " + ex.Message);
                throw;
            }

            // **** inform the user what is going on ****
            Console.WriteLine("ReadFile <<< done reading file!!!");

            // **** return the number of bytes read ****
            return length;
        }
    }
}

The asynchronous method opens and reads all the data in the specified file in an asynchronous mode. The data is collected in a string. The length of the string is then returned to the caller. A run of this code follows:

C:\Users\John\Documents\Visual Studio 2017\Projects\TwoMethods\RealTime\bin\Debug>realtime
ReadFile <<< reading file...
CallMethod <<< before work 1
CallMethod <<< before work 2
CallMethod <<< before work 3
ReadFile <<< done reading file!!!
CallMethod <<< length: 1237487                  <====
CallMethod <<< after work 4
CallMethod <<< after work 5 C:\Users\John\Documents\Visual Studio 2017\Projects\TwoMethods\RealTime\bin\Debug>dir c:\temp\1255_store.txt
 Volume in drive C is OS
 Volume Serial Number is 26E8-87B0

 Directory of c:\temp

03/08/2019  09:24 AM         1,237,487 1255_store.txt
               1 File(s)      1,237,487 bytes   <==== 0 Dir(s) 583,046,819,840 bytes free C:\Users\John\Documents\Visual Studio 2017\Projects\TwoMethods\RealTime\bin\Debug>

As you can see the three lines in the CallMethod method are called in sequence and executed in the specified order. The code then waits for the asynchronous operation of reading the file and returning the number of bytes. The length is displayed. Finally two additional tasks (lines of code) are displayed in order.

Hope this helps understanding asynchronous and synchronous tasks. In general I/O tasks take several orders of magnitude longer than non-I/O operations. When possible make use of parallel tasks using the async and wait types of code markers / annotations depending on the programming language in use.

I placed the source code for the projects in this solution in my GitHub repository.

If you have comments or questions regarding this or any other post in this blog, or if you would like me to help with any phase in the SDLC (Software Development Life Cycle) of a product or service, please do not hesitate and leave me a note below. Requests for help will remain private.

Keep on reading and experimenting. It is the best way to learn!

John

Follow me on Twitter:  @john_canessa

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.