Note: this article is published on 07/08/2024.

This is a series of articles about Multi-threading.

A - Introduction

We already have several articles to discuss the Async programming. This article will be a summary.

In article, Multi-Threading (2), Implementation Overview, we mentioned there are three Async programming patterns, for .NET:

In article, Multi-Threading (2-1), Different MultiThreading Topics, we mentioned .Net has three low-level mechanisms to run Async code in parallel:

These three mechanism serve different purposes.

In this article, we will first briefly discuss the three async low-level machanisms, and conclude that the task machanism is the best choice for the modern programming. In the second section, we will discuss briefly the ways to make the threading programming, while in the last section, we will briefly describe the major features of the task machanism.

This is a summary of the multi-threading issue, will discuss the topics

B - Threads vs. Tasks --- the choice of three async machanism

.Net has three low-level mechanisms to run code in parallel: Thread, ThreadPool, and Task. These three mechanism serve different purposes.

B1 - Thread

B2 - ThreadPool

B3 - Task

B4 - Conclusion

C - The ways to implemente Threading

The major will be like below:

C1 - Asynchronous Programming Model Pattern

APM Pattern (ref)

C2 - Event-Based Asynchronous Pattern

Event-Based Pattern (ref)

C3 - Threading class:

​Thread t = new Thread(worker);
t.Start(); //start a new thread, call the method WorkerThreadMethod

D - Task and async/await

This section will discuss the major features of the task machanism.

D1 - Task

The Task Parallel Library (TPL) is based on the concept of a task, which represents an asynchronous operation. In some ways, a task resembles a thread or ThreadPool work item but at a higher level of abstraction. The term task parallelism refers to one or more independent tasks running concurrently. Tasks provide two primary benefits:

For both reasons, TPL is the preferred API for writing multi-threaded, asynchronous, and parallel code in .NET.

When you create a task, you give it a user delegate that encapsulates the code that the task will execute. The delegate can be expressed as a named delegate, an anonymous method, or a lambda expression. Lambda expressions can contain a call to a named method,

You can use the Task.Start methods to create and start a task in one operation.

You can also use the Task.Run methods to create and start a task in one operation.

Different from using C# async/await keywords to create an async method, we do not need to wait until the await keyword is met the first time to create a new thread and return the original thread back to the calling function. in fact, when Task.Run is call, it is in a new created thread, while the main thread is back to the calling function.

i.e. the behavior of Task.Run is exactly the same as await in an async method is met the first time

Task Methods:

D2 - async and asit keywords

async and await are two new keywords introduced into C# 5.0 in 2012. The async and await keywords are the heart of async programming. By using those two keywords, one can create an asynchronous method almost as easily as creating a synchronous method, even without really understanding the runtime workflow.

Code sample:

Note

E - Task Exception Handling

We have two ways to handle the Task Exceptions:

E1 - try/catch statement

When using one of the static or instance Task.Wait methods the exceptions are propagated, and can be handled by enclosing the call in a try/catch statement:

see: Exception handling (Task Parallel Library) - .NET | Microsoft Learn

E2 - By Task method: FromException

See: Task.FromException Method (System.Threading.Tasks) | Microsoft Learn

This method creates a Task object whose Status property is Faulted and whose Exception property contains exception. The method is commonly used when you immediately know that the work that a task performs will throw an exception before executing a longer code path. For an example, see the FromException<TResult>(Exception) overload.

The following example is a command-line utility that calculates the number of bytes in the files in each directory whose name is passed as a command-line argument. Rather than executing a longer code path that instantiates a FileInfo object and retrieves the value of its FileInfo.Length property for each file in the directory, the example simply calls the FromException<TResult>(Exception) method (Line 39) to create a faulted task if a particular subdirectory does not exist.

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      string[] args = Environment.GetCommandLineArgs();
      if (args.Length > 1) {
         List<Task<long>> tasks = new List<Task<long>>();
         for (int ctr = 1; ctr < args.Length; ctr++)
            tasks.Add(GetFileLengthsAsync(args[ctr]));

         try {
            Task.WaitAll(tasks.ToArray());
         }
         // Ignore exceptions here.
         catch (AggregateException) {}

         for (int ctr = 0 ; ctr < tasks.Count; ctr++) {
            if (tasks[ctr].Status == TaskStatus.Faulted)
               Console.WriteLine("{0} does not exist", args[ctr + 1]);
            else
               Console.WriteLine("{0:N0} bytes in files in '{1}'",
                                 tasks[ctr].Result, args[ctr + 1]);
         }
      }
      else {
         Console.WriteLine("Syntax error: Include one or more file paths.");
      }
   }

   private static Task<long> GetFileLengthsAsync(string filePath)
   {
      if (! Directory.Exists(filePath)) {
         return Task.FromException<long>(
                     new DirectoryNotFoundException("Invalid directory name."));
      }
      else {
         string[] files = Directory.GetFiles(filePath);
         if (files.Length == 0)
            return Task.FromResult(0L);
         else
            return Task.Run( () => { long total = 0;
                                     Parallel.ForEach(files, (fileName) => {
                                                 var fs = new FileStream(fileName, FileMode.Open,
                                                                         FileAccess.Read, FileShare.ReadWrite,
                                                                         256, true);
                                                 long length = fs.Length;
                                                 Interlocked.Add(ref total, length);
                                                 fs.Close(); } );
                                     return total;
                                   } );
      }
   }
}
// When launched with the following command line arguments:
//      subdir . newsubdir
// the example displays output like the following:
//       0 bytes in files in 'subdir'
//       2,059 bytes in files in '.'
//       newsubdir does not exist

The result is as expected (we do not setup the subdir subfolder):

Add one more line code after Line 25 to catch the exception:

then we have the exception printed as

References: