Multi-Threading (2-1), Different MultiThreading Topics

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 of different approaches. 

The content of the article:

  • A - Introduction
  • B - Async/Await - Best Practices in Asynchronous Programming
  • C - Difference between Thread, ThreadPool & BackgroundWorker
  • D - Task vs Thread differences
  • E - Threads vs. Tasks
  • F - Options for Asynchronous Programming
  • G - Multithreading: immutability
  • H - MS Threading Model:
  • I - Asynchronous pattern:

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 the article, Multi-Threading (2), Implementation Overview, while in the second half of the article, I just simply list the topics there, written in this article. For myself, it is still a learning process; for others, one may see how others learn multi-threading.

B - Async/Await - Best Practices in Asynchronous Programming

  • Avoid async void    Prefer async Task methods over async void methods
    • There are three possible return types for async methods: Task, Task<T>, and void, but the natural return types for async methods are just Task and Task<T>. When converting from synchronous to asynchronous code, any method returning a type T becomes an async method returning Task<T>, and any method returning void becomes an async method returning Task.
    • Void-returning async methods have a specific purpose: to make asynchronous event handlers possible.
      • lAsync void methods have different error-handling semantics.
      • Async void methods have different composing semantics.
      • Async void methods are difficult to test.
    • Async void methods have several disadvantages compared to async Task methods, but they’re quite useful in one particular case: asynchronous event handlers.
  • async/await - when to return a Task vs void? - Stack Overflow
    • 1) Normally, you would want to return a Task. The main exception should be when you need to have a void return type (for events). If there's no reason to disallow having the caller await your task, why disallow it?
    • 2) async methods that return void are special in another aspect: they represent top-level async operations and have additional rules that come into play when your task returns an exception. The easiest way is to show the difference is with an example:
  • An async method in C# can have any one of these signatures.
    • 1. async Task MyMethod() { }  which creates a method that can be awaited but does not return any value.
    • 2. async Task<T> MyReturningMethod { return default(T); } which creates a method that can be awaited and returns a value of the type T.
    • 3. async void MyFireAndForgetMethod() { }, which allows for fire and forget methods and cannot be awaited.
  • We should always avoid having a void return type in async methods.
    • async void methods exist for the single purpose of being used as an event handler. More specifically, it has been introduced to support UI elements event handlers, nothing else.

C - Difference between Thread, ThreadPool & BackgroundWorker

  • I do not understand what is the difference between calling any method with the help of a thread class or ThreadPool class
    • The main difference is whether you manage the thread lifetime yourself (Thread) or you take advantage of the "pool" of threads the framework already creates.
    • Using ThreadPool.QueueUserWorkItem will (often) use a thread that already exists in the system. This means you don't have the overhead of spinning up a new thread - instead, a set of threads is already there, and your work is just run on one of them. This can be especially beneficial if you're doing many operations which are short-lived.
    • When you use a new Thread, you actually spin up a new thread that will live until your delegate completes its execution.
    • Note that, as of .NET 4 and 4.5, I'd recommend using the Task and Task<T> types instead of making your own threads or using ThreadPool.QueueUserWorkItem. These (by default) use the thread pool to execute but provide many other useful abstractions, especially with C# 5 and the await/async keywords.
  • Now also tell me what is BackgroundWorker class and how it is different from Thread & ThreadPool class.
    • The BackgroundWorker class is an abstraction over the thread pool. It uses the ThreadPool to queue up "work" (your DoWork event handler) but also provides extra functionality which allows progress and completion events to be posted back to the initial SynchronizationContext (which, in a GUI program, will typically be the user interface thread). This simplifies tasks where you want to update a UI for progress or completion notifications as background "work" is running on a background thread.

D - Task vs Thread differences

Thread is a lower-level concept: if you're directly starting a thread, you know it will be a separate thread rather than executing on the thread pool, etc.

The task is more than just an abstraction of "where to run some code" though - it's really just "the promise of a result in the future". So as some different examples:

  • Task.Delay doesn't need any actual CPU time; it's just like setting a timer to go off in the future
  • A task returned by WebClient.DownloadStringTaskAsync won't take much CPU time locally; it's representing a result that is likely to spend most of its time in network latency or remote work (at the web server)
  • A task returned by Task.Run() really is saying, "I want you to execute this code separately"; the exact thread on which that code executes depends on a number of factors.

Note that the Task<T> abstraction is pivotal to the async support in C# 5.

In general, I'd recommend that you use the higher level abstraction wherever you can: in modern C# code, you should rarely need to explicitly start your own thread.

E - Threads vs. Tasks

  • Thread
    • The thread represents an actual OS-level thread with its own stack and kernel resources. (technically, a CLR implementation could use fibers instead, but no existing CLR does this). The thread allows the highest degree of control; you can Abort() or Suspend() or Resume() a thread (though this is a very bad idea), you can observe its state, and you can set thread-level properties like the stack size, apartment state, or culture.
    • The problem with Thread is that OS threads are costly. Each thread you have consumes a non-trivial amount of memory for its stack and adds additional CPU overhead as the processor context-switch between threads. Instead, it is better to have a small pool of threads execute your code as work becomes available.
    • There are times when there is no alternative Thread. If you need to specify the name (for debugging purposes) or the apartment state (to show a UI), you must create your own Thread (note that having multiple UI threads is generally a bad idea). Also, if you want to maintain an object that is owned by a single thread and can only be used by that thread, it is much easier to explicitly create a Thread instance for it so you can easily check whether the code trying to use it is running on the correct thread.
  • ThreadPool
    • ThreadPool is a wrapper around a pool of threads maintained by the CLR. ThreadPool gives you no control at all; you can submit work to execute at some point, and you can control the size of the pool, but you can't set anything else. You can't even tell when the pool will start running the work you submit to it.
    • Using ThreadPool avoids the overhead of creating too many threads. However, if you submit too many long-running tasks to the threadpool, it can get full, and later work that you submit can end up waiting for the earlier long-running items to finish. In addition, the ThreadPool offers no way to find out when a work item has been completed (unlike Thread.Join()), nor a way to get the result. Therefore, ThreadPool is best used for short operations where the caller does not need the result.
  • Task
    • Finally, the Task class from the Task Parallel Library offers the best of both worlds. Like the ThreadPool, a task does not create its own OS thread. Instead, tasks are executed by a TaskScheduler; the default scheduler simply runs on the ThreadPool.
    • Unlike the ThreadPool, Task also allows you to find out when it finishes and (via the generic Task) to return a result. You can call ContinueWith() on an existing Task to make it run more code once the task finishes (if it's already finished, it will run the callback immediately). If the task is generic, ContinueWith() will pass you the task's result, allowing you to run more code that uses it.
    • You can also synchronously wait for a task to finish by calling Wait() (or, for a generic task, by getting the Result property), like Thread.Join(); this will block the calling thread until the task finishes. Synchronously waiting for a task is usually a bad idea; it prevents the calling thread from doing any other work and can also lead to deadlocks if the task ends up waiting (even asynchronously) for the current thread.
    • Since tasks still run on the ThreadPool, they should not be used for long-running operations since they can still fill up the thread pool and block new work. Instead, Task provides a LongRunning option, which will tell the TaskScheduler to spin up a new thread rather than run on the ThreadPool.
    • All newer high-level concurrency APIs, including the Parallel.For*() methods, PLINQ, C# 5 await, and modern async methods in the BCL, are all built on Task.
  • Conclusion
    • ​​​​​​​The bottom line is that Task is almost always the best option; it provides a much more powerful API and avoids wasting OS threads.
    • The only reasons to explicitly create your own Threads in modern code are setting per-thread options or maintaining a persistent thread that needs to maintain its own identity.

F - Options for Asynchronous Programming (Pro .NET 2.0 WinForms, P695)

  • 1. Asynchronous delegate calls:
    • ​​​​​​​Marshalling Calls to the Right Thread (Pro .NET 2.0 WinForms, P705):
    • ​​​​​​​To marshaling back to UI:
      • ​​​​​​​Use Control.Invoke, for WinForms,
      • Use Dispatcher.BeginInvoke for WPF
  • 2. The backgroundWorker component;
    • ​​​​​​​Supporting progress and cancel features.
    • Using System.ComponentModel.BackgroundWorker class;
    • It works the same way as the delegate approach, but the marshaling issues are abstracted away with an event-based model.
      • ​​​​​​​Methods:
        • ​​​​​​​RunWorkerAsync: starting work in the background asynchronously
        • CancelAsync
      • ​​​​​​​Events:
      • DoWork: This occurs when RunWorkerAsync is called. --- Starting your work
      • ProgressChange
      • RunWorkerCompleted
  • ​​​​​​​​​​​​​​3. The System.Threading.Thread class.

G - Multithreading: immutability

If we build immutable types, then everything should be a lot easier, and we don’t need to synchronize access to these items.

H - MS Threading Model

  • See the Single-Threaded Application with Long-Running Calculation Sample
  • It shows the difference below:
    • ​​​​​​​What’s the difference between Invoke() and BeginInvoke()
      • ​​​​​​​Do you mean Delegate?Invoke/BeginInvoke or Control.Invoke/BeginInvoke?
    • ​​​​​​​Delegate.Invoke: Executes synchronously on the same thread.
    • Delegate.BeginInvoke: Executes asynchronously on a threadpool thread.
    • Control.Invoke: Executes on the UI thread, but calling (UI) thread waits for completion before continuing.
    • Control.BeginInvoke: Executes on the UI thread, and calling (UI) thread doesn't wait for completion.
  • ​​​​​​​​​​​​​​Remarks
    • ​​​​​​​The delegate is called asynchronously, and this method returns immediately. You can call this method from any thread, even the thread that owns the control's handle. If the control's handle does not exist yet, this method searches up the control's parent chain until it finds a control or form that does have a window handle. If no appropriate handle can be found, BeginInvoke will throw an exception. Exceptions within the delegate method are considered untrapped and will be sent to the application's untrapped exception handler.
    • You can call EndInvoke to retrieve the return value from the delegate if necessary, but this is not required. EndInvoke will block until the return value can be retrieved.

I - Asynchronous pattern

  • MethodNameAsync
  • MethodNameAsyncCancel
  • MethodNameCompleted
  • MethodNameProgressChanged

References:

 


Recommended Free Ebook
Similar Articles