Tuesday, July 28, 2020

What is the difference between asynchronous programming and multi-threading?

This misunderstanding is extremely common. Many people are taught that multithreading and asynchrony are the same things, but they are not.

An analogy usually helps. You are cooking in a restaurant. An order comes in for eggs and toast.
  • Synchronous: you cook the eggs, then you cook the toast.
  • Asynchronous, single-threaded: you start the eggs cooking and set a timer. You start the toast cooking and set a timer. While they are both cooking, you clean the kitchen. When the timers go off, you take the eggs off the heat and the toast out of the toaster and serve them.
  • Asynchronous, multithreaded: you hire two more cooks, one to cook eggs and one to cook toast. Now you have the problem of coordinating the cooks so that they do not conflict with each other in the kitchen when sharing resources. And you have to pay for them.

Now, does it make sense that multithreading is only one kind of synchrony?

Threading is about workers; asynchrony is about tasks. In multithreaded workflows, you assign tasks to workers. In asynchronous single-threaded workflows, you have a graph of tasks where some tasks depend on the results of others; as each task completes it invokes the code that schedules the next task that can run, given the results of the just-completed task. But you (hopefully) only need one worker to perform all the tasks, not one worker per task.

It will help to realize that many tasks are not processor-bound. For processor-bound tasks, it makes sense to hire as many workers (threads) as there are processors, assign one task to each worker, assign one processor to each worker, and have each processor do the job of nothing else but compute the result as quickly as possible. But for tasks that are not waiting on a processor, you don't need to assign a worker at all. You just wait for the message to arrive that the result is available and do something else while you're waiting. When that message arrives, then you can schedule the continuation of the completed task as the next thing on your to-do list to check off.

So let's look at Jon's example in more detail. What happens?
  • Someone invokes DisplayWebSiteLength. Who? We don't care.
  • It sets a label, creates a client, and asks the client to fetch something. The client returns an object representing the task of fetching something. That task is in progress.
  • Is it in progress on another thread? Probably not. Read Stephen's article on why there is no thread.
  • Now we await the task. What happens? We check to see if the task has been completed between the time we created it, and we awaited it. If yes, then we fetch the result and keep running. Let's suppose it has not been completed. We sign up the remainder of this method as the continuation of that task and return.
  • Now control has returned to the caller. What does it do? Whatever it wants.
  • Now suppose the task completes. How did it do that? Maybe it was running on another thread, or maybe the caller that we just returned to allowed it to run to completion on the current thread. Regardless, we now have a completed task.
  • The completed task asks the correct thread -- again, likely the only thread -- to run the continuation of the task.
  • Control passes immediately back into the method we just left at the point of the await. Now there is a result available so we can assign text and run the rest of the method.

It's just like in my analogy. Someone asks you for a document. You send away in the mail for the document and keep on doing other work. When it arrives in the mail you are signalled, and when you feel like it, you do the rest of the workflow -- open the envelope, pay the delivery fees, whatever. You don't need to hire another worker to do all that for you.

Answered by: Eric Lippert on StackOverflow

Code sample :

class Program
{
    static void Main(string[] args)
    {
        using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource())
        {
            Console.CancelKeyPress += (s, e) =>
            {
               e.Cancel = true;
               Console.WriteLine("Stopped by user");
               cancellationTokenSource.Cancel(true);
            };
            try
            {
               MainAsync(cancellationTokenSource).GetAwaiter().GetResult();
            }
            catch (OperationCanceledException) 
            {
               Console.WriteLine("Operation has been cancelled by the user.");
               Console.ReadLine();
            }
         }
     }}
     private static async Task MainAsync(CancellationTokenSource cancellationTokenSource)
     {
         Console.WriteLine("Exercise 1 :");
         var tasks = new List<Task>();
         var resourceList = new string[] { "resourceA", "resourceB", "resourceC" };

         IEnumerable<Task<string>> getLengthTask =
                from resorce in resourceList
                select GetResourceContentAsync(resorce, cancellationTokenSource.Token);
         
         Console.WriteLine("Press CTRL+C at anytime to Cancel");
         Console.WriteLine("Getting data from remote server...");
            
         var content = await Task.WhenAll(getLengthTask.ToArray());

         Console.WriteLine($"Total Content Length : {content.ToList().Sum(c => c.Length)}");
         Console.ReadLine();

     }

     private static async Task<string> GetResourceContentAsync(string resourceId, CancellationToken token)
     {
          HttpClient client = new HttpClient();
          Console.WriteLine($"Value has been requested from '{resourceId}'");
          var responseMessage = await client.GetAsync($"https://localhost:44345/api/{resourceId}", token);
          Console.WriteLine($"Response has been received from '{resourceId}'");
          return await responseMessage.Content.ReadAsStringAsync();
      }