Multi-Threading (2), Implementation Overview

This is a series of articles about Multi-threading,

A - Introduction

The first article in this series, Multi-Threading (1), Concept: What, Why, written on 01/30/2023, introduced the multi-threading concept. The following articles should be the implementations in different approaches. 

The content of the article:

  • A - Introduction
  • B - Asynchronous programming patterns
  • C - Ways to implement Multithreading in C#
  • D - Using Thread class to do Multithreading and Asynchronous callback
  • E - Manage Thread Safety and Synchronization

Note

Writing a summary article is somewhat quite difficult. Before one jumps into each topic, it is hard to have an overview; on the other hand, if writing a series of articles, when one makes them done, it seems not necessary to write a summary article.

This article on MultiThreading overview is actually my study notes recorded over decades. In the first half, I tried to make them logical, as written in this article, while in the second half of the article, I just simply list the topics there, written in another article, Multi-Threading (2-1), Different MultiThreading Topics. For myself, it is still a learning process; for others, one may see how others learn multi-threading.

B - Asynchronous programming patterns

What are Asynchronous programming patterns? We start from Microsoft definition, that is specifically indicating to .Net Environment:

  • Asynchronous programming patterns | Microsoft Learn
    • .NET provides three patterns for performing asynchronous operations:
      • Task-based Asynchronous Pattern (TAP), which uses a single method to represent the initiation and completion of an asynchronous operation. TAP was introduced in .NET Framework 4. It's the recommended approach to asynchronous programming in .NET. The async and await keywords in C# and the Async and Await operators in Visual Basic add language support for TAP. For more information, see Task-based Asynchronous Pattern (TAP).
      • Event-based Asynchronous Pattern (EAP), which is the event-based legacy model for providing asynchronous behavior. It requires a method that has the Async suffix and one or more events, event handler delegate types, and EventArg-derived types. EAP was introduced in .NET Framework 2.0. It's no longer recommended for new development. For more information, see Event-based Asynchronous Pattern (EAP).
      • Asynchronous Programming Model (APM) pattern (also called the IAsyncResult pattern) is the legacy model that uses the IAsyncResult interface to provide asynchronous behavior. In this pattern, asynchronous operations require Begin and End methods (for example, BeginWrite and EndWrite to implement an asynchronous write operation). This pattern is no longer recommended for new development. For more information, see Asynchronous Programming Model (APM).

The major features of these Asynchronous Programming Models:

Asynchronous Programming Model Pattern

  • A native or raw asynchronous programming model using a delegate to create a thread. such that you define a delegate with the same signature as the method you want to call, the common language runtime automatically defines BeginInvoke and EndInvoke methods for this delegate, with the appropriate signatures. Therefore, we could say APM is a delegate-based Asynchronous Programming model.

APM Pattern (ref)

Event-Based Asynchronous Pattern

  • The Event-based Asynchronous Pattern has a single MethodNameAsync method and a corresponding MethodNameCompleted event

  • Basically, this pattern enforces a pair of methods and an event to collaborate and help the application execute a thread asynchronously

Event-Based Pattern (ref)

Task-based Asynchronous Pattern

  • The Microsoft .NET Framework 4.0 introduces a new Task Parallel Library (TPL) for parallel computing and asynchronous programming. The namespace is "System.Threading.Tasks".

  • A Task can represent an asynchronous operation, and a Task provides an abstraction over creating and pooling threads.

Task-Based Pattern (ref)

C - Ways to implement Multithreading in C#

// Passing the method name to be executed in the ctor directly without using the ThreadStart delegate

         var thread1 = new Thread(DoSomeWork);
         thread1.Start();

 // Passing the ThreadStart delegate, which points to a method to be executed

         var threadStart = new ThreadStart(DoSomeWork);
         var thread2 = new Thread(threadStart);
         thread2.Start();

// Passing the ParametrizedThreadStart delegate, which points to the method to be executed

         var parametrizedThreadStart = new ParameterizedThreadStart(DoSomeWorkWithParameter);
         var thread3 = new Thread(parametrizedThreadStart);
         thread3.Start(2);

// Passing a Lambda expression in the Thread class constructor and subsequently calling the Start method

         var thread4 = new Thread(() =>
         {
             int x = 5;
             for (int i = 0; i < x; i++)
             {
                 Console.WriteLine(i);
             }
         });

         thread4.Start();

// Leveraging ThreadPools, call ThreadPool.QueueUserWorkItem passing in the method name to be executed

         ThreadPool.QueueUserWorkItem(DoSomeWorkWithParameter);
         ThreadPool.QueueUserWorkItem(DoSomeWorkWithParameter, 4);

// Using TPL (Task Parallel Library). Create a Task<T>, where T is the return type of the method to be executed.

         Task<string> task = Task.Factory.StartNew<string>(DoSomeStringWork);
         var result = task.Result;

// Instantiating Task class directly

         var task = new Task(
                () =>
                {
                    Console.WriteLine("Worker thread!");
                    Thread.Sleep(1000);
                });

            task.Start();

// Using Asynchronous Delegates, also known as the APM pattern 

Func<string, string> work = DoSomeStringWork;
IAsyncResult res = work.BeginInvoke("Hello", null, null);
string result1 = work.EndInvoke(res);

D - Using Thread class to do Multithreading and Asynchronous callback

ThreadStart is a system-defined delegate that represents the method that executes on the Thread.

System.Threading.ThreadStart worker = new ThreadStart(WorkerThreadMethod);

Two ways to instantiate a Thread:

  • Way 1: creating a new Thread
    • Thread t = new Thread(worker);
      t.Start(); //start a new thread, call the method WorkerThreadMethod
  • Way 2: Obtain the CurrentThread
    • Thread current = Thread.CurrentThread;

Manage Thread lifetime: We use thread class properties to manage thread lifetime:

  • Start --- Start a thread
  • Sleep(TimeSpan) --- block the thread at a certain time (Static Method, only can be called from within the current thread)
  • Join --- Blocks the calling thread until the called thread terminates
  • Suspend/ Resume --- Suspend/Resume the thread
  • Interrupt --- Interrupts a thread that is in the WaitSleepJoin thread state
  • Abort --- terminate the thread, throwing a ThreadAobrtException

E - Manage Thread Safety and Synchronization:

To handle the atomicity and avoid the deadlock.

  • Atomicity: one account can be accessed only by one thread at a time to avoid the race condition (unexpected change by more than one thread accessing the same account at the same time).
  • Interlocked Class: Provides atomic operations for variables that are shared by multiple threads.
  • Interlocked Class: You can use the methods of the Interlocked class to prevent problems that can occur when multiple threads attempt to simultaneously update or compare the same value. The methods of this class let you safely increment, decrement, exchange, and compare values from any thread.
  • ReaderWriter Locks: In some cases, you may want to lock a resource only when data is being written and permit multiple clients to simultaneously read data when data is not being updated. The ReaderWriterLock class enforces exclusive access to a resource while a thread is modifying the resource, but it allows non-exclusive access when reading the resource. ReaderWriter locks are a useful alternative to exclusive locks, which cause other threads to wait, even when those threads do not need to update data.
  • Deadlock: To avoid the race condition, one needs to lock the thread to guarantee atomicity. However, this could cause another problem, the deadlock, i.e., each of the threads waiting for others to release.
  • System.Monitor class enables serialized access to blocks of code by means of locks and signals
    • Monitor.Enter(this);
      …
      … // a lock on the code
      …
      Monitor.Exit(this);

      Note: you must place the Exit call in a finally block; otherwise, the thread will be handed up when an exception occurs.

  • C#: lock statement, this resulted in a try…finally block being inserted into code by CLR, preventing from hanging up the thread.
    • lock(this)
      {
      …
      }

References: