Table of Contents
- 1. Get Current Thread Information
- 2. Manage Thread States
- 2.1.3 Delegates and Lambda
- 2.2 Pause and Block
- 2.3 Thread States
- 2.4 Termination
- 2.5 Thread Uncertainty
- 2.6 Thread Priority, Foreground and Background Threads
- 2.7 Spin and Sleep
This article is the first in the series “Introduction to Multithreading and Practices (Beginner)”, which covers the simplest and most familiar concepts. As part of this series, I will begin with the most basic aspects and continuously learn and explore multithreading in C# together with you all.
Theoretical discussions will not be extensive here. More in-depth topics will be addressed in the intermediate series, where a lot of knowledge related to the underlying layers will be discussed.
Typically, a series of articles begins with some words of encouragement, right?
So I wish everyone to study hard and strive for progress every day.
The first step in learning multithreading is to learn about the Thread class. The Thread class can create and control threads, set their priority, and get their status. This article will start with the creation and lifecycle of threads.
Official documentation on the Thread class, detailing its properties and methods:
https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.thread?view=netcore-3.1#properties
Now, open your Visual Studio
and let's write some code together.
1. Get Current Thread Information
Thread.CurrentThread
is a static property of the Thread class that can retrieve information about the thread currently running, defined as follows:
public static System.Threading.Thread CurrentThread { get; }
The Thread class has many properties and methods, which will become more familiar as we continue learning about more APIs in the future.
Here’s a simple example:
static void Main(string[] args)
{
Thread thread = new Thread(OneTest);
thread.Name = "Test";
thread.Start();
Console.ReadKey();
}
public static void OneTest()
{
Thread thisThread = Thread.CurrentThread;
Console.WriteLine("Thread Identifier: " + thisThread.Name);
Console.WriteLine("Current Culture: " + thisThread.CurrentCulture.Name); // Current Culture
Console.WriteLine("Thread Execution Status: " + thisThread.IsAlive);
Console.WriteLine("Is Background Thread: " + thisThread.IsBackground);
Console.WriteLine("Is Thread Pool Thread: " + thisThread.IsThreadPoolThread);
}
Output:
Thread Identifier: Test
Current Culture: zh-CN
Thread Execution Status: True
Is Background Thread: False
Is Thread Pool Thread: False
2. Manage Thread States
There are generally five states for a thread:
New (new object), Ready (waiting for CPU scheduling), Running (CPU is running), Blocked (waiting for blocking, synchronization blocking, etc.), and Dead (object released).
Let’s skip the theory for now and jump straight into coding.
2.1 Starting and Passing Parameters
Creating a new thread is quite straightforward—just new
it up and call Start()
.
Thread thread = new Thread();
There are four constructors for the Thread class:
public Thread(ParameterizedThreadStart start);
public Thread(ThreadStart start);
public Thread(ParameterizedThreadStart start, int maxStackSize);
public Thread(ThreadStart start, int maxStackSize);
Let’s discuss how to pass parameters when starting a new thread, using these four constructors.
2.1.1 ParameterizedThreadStart
ParameterizedThreadStart is a delegate where the constructor passes the method to be executed, and parameters are passed in the Start
method.
It's important to note that the parameter type is object and only one parameter can be passed.
Here’s a code example:
static void Main(string[] args)
{
string myParam = "abcdef";
ParameterizedThreadStart parameterized = new ParameterizedThreadStart(OneTest);
Thread thread = new Thread(parameterized);
thread.Start(myParam);
Console.ReadKey();
}
public static void OneTest(object obj)
{
string str = obj as string;
if (string.IsNullOrEmpty(str))
return;
Console.WriteLine("A new thread has started");
Console.WriteLine(str);
}
2.1.2 Using Static Variables or Class Member Variables
This method does not require parameters to be passed; each thread shares a stack.
The advantage is that no boxing/unboxing is needed, and multiple threads can share space; the downside is that the variables are accessible by everyone, which could lead to various issues in multithreaded competition (lock mechanisms can help solve this).
Here’s an example using two variables to achieve data transfer:
class Program
{
private string A = "Member Variable";
public static string B = "Static Variable";
static void Main(string[] args)
{
// Create an instance of the class
Program p = new Program();
Thread thread1 = new Thread(p.OneTest1);
thread1.Name = "Test1";
thread1.Start();
Thread thread2 = new Thread(OneTest2);
thread2.Name = "Test2";
thread2.Start();
Console.ReadKey();
}
public void OneTest1()
{
Console.WriteLine("A new thread has started");
Console.WriteLine(A); // Other members of the same object
}
public static void OneTest2()
{
Console.WriteLine("A new thread has started");
Console.WriteLine(B); // Global static variable
}
}
2.1.3 Delegates and Lambda
The principle here is in the Thread constructor public Thread(ThreadStart start);
, where ThreadStart
is a delegate defined as follows:
public delegate void ThreadStart();
By using delegates, we can write code like this:
static void Main(string[] args)
{
System.Threading.ThreadStart start = DelegateThread;
Thread thread = new Thread(start);
thread.Name = "Test";
thread.Start();
Console.ReadKey();
}
public static void DelegateThread()
{
OneTest("a", "b", 666, new Program());
}
public static void OneTest(string a, string b, int c, Program p)
{
Console.WriteLine("A new thread has started");
}
It may be a bit cumbersome, but we can quickly implement it using Lambda.
Here’s a Lambda example:
static void Main(string[] args)
{
Thread thread = new Thread(() =>
{
OneTest("a", "b", 666, new Program());
});
thread.Name = "Test";
thread.Start();
Console.ReadKey();
}
public static void OneTest(string a, string b, int c, Program p)
{
Console.WriteLine("A new thread has started");
}
As you can see, C# is incredibly convenient.
2.2 Pause and Block
The Thread.Sleep()
method can suspend the current thread for a certain period, while the Thread.Join()
method can block the current thread until another thread finishes execution.
During the waiting process with Sleep()
or Join()
, the thread is in a blocked state.
The definition of blocking: When a thread is paused due to certain conditions, it is considered blocked.
If a thread is in a blocked state, it will yield its CPU time slice and will not consume CPU time until the block is lifted.
Blocking entails context switching.
Here’s an example code:
static void Main(string[] args)
{
Thread thread = new Thread(OneTest);
thread.Name = "Little Brother";
Console.WriteLine($"{DateTime.Now}: Everyone is having dinner, and after dinner, little brother will go shopping.");
Console.WriteLine("Dinner is ready");
Console.WriteLine($"{DateTime.Now}: Little brother starts playing a game");
thread.Start();
// Makeup for 5 seconds
Console.WriteLine("Ignore him; the big sister will do her makeup first.");
Thread.Sleep(TimeSpan.FromSeconds(5));
Console.WriteLine($"{DateTime.Now}: Makeup done, waiting for little brother to finish his game.");
thread.Join();
Console.WriteLine("Did he finish the game? " + (!thread.IsAlive ? "true" : "false"));
Console.WriteLine($"{DateTime.Now}: Let's go shopping.");
Console.ReadKey();
}
public static void OneTest()
{
Console.WriteLine(Thread.CurrentThread.Name + " starts playing a game");
for (int i = 0; i < 10; i++)
{
Console.WriteLine($"{DateTime.Now}: Round number: {i}");
Thread.Sleep(TimeSpan.FromSeconds(2)); // Sleep for 2 seconds
}
Console.WriteLine(Thread.CurrentThread.Name + " has finished playing");
}
2.3 Thread States
ThreadState
is an enumeration that records the state of the thread, from which we can determine the lifecycle and health status of the thread.
The enumeration is as follows:
| Enumeration | Value | Description |
|--------------|-------|------------------------------------------------------------------|
| Initialized | 0 | This state indicates that the thread has been initialized but has not yet started. |
| Ready | 1 | This state indicates that the thread is waiting to use the processor due to lack of available resources. The thread is ready to run on the next available processor. |
| Running | 2 | This state indicates that the thread is currently using the processor. |
| Standby | 3 | This state indicates that the thread is about to use the processor. Only one thread can be in this state at a time. |
| Terminated | 4 | This state indicates that the thread has completed execution and has exited. |
| Transition | 6 | This state indicates that the thread is waiting for resources outside the processor before it can execute. For example, it might be waiting for its execution stack to be paged from disk. |
| Unknown | 7 | The state of the thread is unknown. |
| Wait | 5 | This state indicates that the thread is not ready to use the processor because it is waiting for an external operation to complete or waiting for resources to be released. When the thread is ready, it will be re-queued.
。
However, there are many enumerated types that are not useful. We can use a method like this to get more useful information:
public static ThreadState GetThreadState(ThreadState ts)
{
return ts & (ThreadState.Unstarted |
ThreadState.WaitSleepJoin |
ThreadState.Stopped);
}
Based on the example in 2.2, we modify the method in Main:
static void Main(string[] args)
{
Thread thread = new Thread(OneTest);
thread.Name = "Little Brother";
Console.WriteLine($"{DateTime.Now}: Everyone is having a meal, after the meal we need to take Little Brother shopping.");
Console.WriteLine("The meal is finished.");
Console.WriteLine($"{DateTime.Now}: Little Brother starts playing games.");
Console.WriteLine("What is Little Brother doing? (Thread state): " + Enum.GetName(typeof(ThreadState), GetThreadState(thread.ThreadState)));
thread.Start();
Console.WriteLine("What is Little Brother doing? (Thread state): " + Enum.GetName(typeof(ThreadState), GetThreadState(thread.ThreadState)));
// Makeup for 5 seconds
Console.WriteLine("Forget about him, Big Sister is doing her makeup first.");
Thread.Sleep(TimeSpan.FromSeconds(5));
Console.WriteLine("What is Little Brother doing? (Thread state): " + Enum.GetName(typeof(ThreadState), GetThreadState(thread.ThreadState)));
Console.WriteLine($"{DateTime.Now}: Makeup is done, waiting for Little Brother to finish his game.");
thread.Join();
Console.WriteLine("What is Little Brother doing? (Thread state): " + Enum.GetName(typeof(ThreadState), GetThreadState(thread.ThreadState)));
Console.WriteLine("Is he done with the game? " + (!thread.IsAlive ? "true" : "false"));
Console.WriteLine($"{DateTime.Now}: Let's go shopping.");
Console.ReadKey();
}
The code looks a bit messy, make sure to copy it into your project and run it.
Sample output:
2020/4/11 11:01:48: Everyone is having a meal, after the meal we need to take Little Brother shopping.
The meal is finished.
2020/4/11 11:01:48: Little Brother starts playing games.
What is Little Brother doing? (Thread state): Unstarted
What is Little Brother doing? (Thread state): Running
Forget about him, Big Sister is doing her makeup first.
Little Brother starts playing games.
2020/4/11 11:01:48: Round: 0
2020/4/11 11:01:50: Round: 1
2020/4/11 11:01:52: Round: 2
What is Little Brother doing? (Thread state): WaitSleepJoin
2020/4/11 11:01:53: Makeup is done, waiting for Little Brother to finish his game.
2020/4/11 11:01:54: Round: 3
2020/4/11 11:01:56: Round: 4
2020/4/11 11:01:58: Round: 5
2020/4/11 11:02:00: Round: 6
2020/4/11 11:02:02: Round: 7
2020/4/11 11:02:04: Round: 8
2020/4/11 11:02:06: Round: 9
Little Brother is done.
What is Little Brother doing? (Thread state): Stopped
Is he done with the game? true
2020/4/11 11:02:08: Let's go shopping.
You can see the four states: Unstarted
, WaitSleepJoin
, Running
, and Stopped
, indicating not started (ready), blocked, running, and dead.
2.4 Termination
The .Abort()
method cannot be used on .NET Core, otherwise you will encounter System.PlatformNotSupportedException: "Thread abort is not supported on this platform."
.
Later articles on asynchronous programming will explain how to achieve termination.
Since .NET Core does not support it, we will not discuss these two methods further. Here we only list the APIs without examples.
| Method | Description |
| ------------- | ------------------------------------------------------------ |
| Abort() | Raises ThreadAbortException on the calling thread to begin the process of terminating that thread. Calling this method will usually terminate the thread. |
| Abort(Object) | Raises a ThreadAbortException in the thread on which it is called to begin handling the termination of the thread, while providing exception information regarding the thread termination. Calling this method will usually terminate the thread. |
The Abort()
method injects a ThreadAbortException
into the thread, causing the program to be terminated. However, it does not guarantee termination of the thread.
2.5 Uncertainty of Threads
The uncertainty of threads refers to several parallel running threads, with uncertainty over which thread will receive the CPU time slice next (of course, allocation has priority).
For us, multithreading is running simultaneously
, but in general, the CPU does not have that many cores, so it is impossible to execute all threads at the same time. The CPU decides which thread to allocate the time slice to at any given moment, leading to CPU time slice allocation scheduling.
Running the following code example, you can see that the order in which the two threads print is uncertain, and the results vary each time you run it.
The CPU has a set of formulas to determine who will receive the next time slice, but they are complex and require learning about computer organization principles and operating systems.
We'll leave that for a future article.
static void Main(string[] args)
{
Thread thread1 = new Thread(Test1);
Thread thread2 = new Thread(Test2);
thread1.Start();
thread2.Start();
Console.ReadKey();
}
public static void Test1()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Test1:" + i);
}
}
public static void Test2()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Test2:" + i);
}
}
2.6 Thread Priority, Foreground and Background Threads
The Thread.Priority
property is used to set the thread's priority. Priority
is a ThreadPriority enumeration, as detailed below.
| Enum | Value | Description |
| ------------- | ----- | ---------------------------------------------------------- |
| AboveNormal | 3 | Can be scheduled after threads with Highest
priority, but before those with Normal
priority. |
| BelowNormal | 1 | Can be scheduled after threads with Normal
priority, but before those with Lowest
priority. |
| Highest | 4 | Can be scheduled before any other priority thread. |
| Lowest | 0 | Can be scheduled after any other priority thread. |
| Normal | 2 | Can be scheduled after threads with AboveNormal
priority, but before those with BelowNormal
priority. By default, threads have Normal
priority. |
Priority order: Highest
> AboveNormal
> Normal
> BelowNormal
> Lowest
.
The Thread.IsBackground
property can be set to specify whether the thread is a background thread.
Foreground threads have a higher priority than background threads, and the program needs to wait for all foreground threads to finish before it can close. However, when the program closes, it will forcibly exit regardless of whether background threads are running.
2.7 Spinning and Sleeping
When a thread is in a sleep state or is being awakened, context switching occurs, which comes with costly overhead.
Conversely, when a thread keeps running, it will consume CPU time and occupy CPU resources.
For very short waits, a spinning method should be used to avoid context switching; for longer waits, the thread should sleep to avoid consuming large amounts of CPU time.
We can use the well-known Sleep()
method to put a thread to sleep. Many types of synchronized threads also use sleeping methods to wait for threads (drafts have already been written for this).
Spinning means looking for something to do.
For example:
public static void Test(int n)
{
int num = 0;
for (int i = 0; i < n; i++)
{
num += 1;
}
}
By doing some simple calculations, time is consumed to achieve the purpose of waiting.
C# has spin locks and the Thread.SpinWait();
method regarding spinning, which will be discussed in the later thread synchronization section.
Thread.SpinWait()
can be useful in extremely rare cases for avoiding thread context switch. It is defined as follows:
public static void SpinWait(int iterations);
SpinWait essentially uses a very tight loop and uses the iterations
parameter to specify the loop count. The wait time for SpinWait depends on the processor's speed.
文章评论