SoFunction
Updated on 2025-03-07

Brief discussion on the explanation of simple examples of C# multithreading

.NET defines the functionality about multithreading in a namespace. Therefore, to use multithreading, you must first declare the reference to this namespace (using ;).

a. Start the thread

As the name suggests, "start thread" means creating and starting a thread, as the following code can be implemented:

Thread thread1 = new Thread(new ThreadStart( Count));

The Count is the function to be executed by the new thread.

b. Kill thread

"Kill a thread" means to eradicate a thread. In order not to waste your energy, it is best to judge whether it is still alive before killing a thread (through the IsAlive property), and then you can call the Abort method to kill the thread.

c. Pause thread

It means to let a running thread sleep for a while. For example (1000); is to let the thread sleep for 1 second.

d. Priority

There is no need to explain this. The hreadPRiority property in the Thread class is used to set priority, but it does not guarantee that the operating system will accept this priority. A thread has 5 priority levels: Normal, AboveNormal, BelowNormal, Highest, Lowest. Specific implementation examples are as follows:

 = ;

e. Suspend thread

The Suspend method of the Thread class is used to suspend the thread until Resume is called, and the thread can not continue to execute. If the thread has been hanged, it won't work.

if ( = ) 
{
();
}

f. Recover thread

Used to restore the already suspended thread to allow it to continue execution. If the thread does not hang, it will not work.

if ( = ) 
{
();
}

An example will be listed below to illustrate simple thread processing capabilities. This example comes from the help documentation.

using System;
using ;

// Simple threading scenario: Start a static method running
// on a second thread.
public class ThreadExample {
// The ThreadProc method is called when the thread starts.
// It loops ten times, writing to the console and yielding 
// the rest of its time slice each time, and then ends.
public static void ThreadProc() {
for (int i = 0; i < 10; i++) {
("ThreadProc: {0}", i);
// Yield the rest of the time slice.
(0);
}
}

public static void Main() {
("Main thread: Start a second thread.");
// The constructor for the Thread class requires a ThreadStart 
// delegate that represents the method to be executed on the 
// thread. C# simplifies the creation of this delegate.
Thread t = new Thread(new ThreadStart(ThreadProc));
// Start ThreadProc. On a uniprocessor, the thread does not get 
// any processor time until the main thread yields. Uncomment 
// the  that follows () to see the difference.
();
//(0);

for (int i = 0; i < 4; i++) {
("Main thread: Do some work.");
(0);
}

("Main thread: Call Join(), to wait until ThreadProc ends.");
();
("Main thread:  has returned. Press Enter to end program.");
();
}
}

The output generated by this code is similar to the following:

Main thread: Start a second thread.
Main thread: Do some work.
ThreadProc: 0
Main thread: Do some work.
ThreadProc: 1
Main thread: Do some work.
ThreadProc: 2
Main thread: Do some work.
ThreadProc: 3
Main thread: Call Join(), to wait until ThreadProc ends.
ThreadProc: 4
ThreadProc: 5
ThreadProc: 6
ThreadProc: 7
ThreadProc: 8
ThreadProc: 9
Main thread:  has returned. Press Enter to end program.

In Visul C#, the namespace provides some classes and interfaces that enable multi-thread programming. There are three methods for creating threads: Thread, ThreadPool, and Timer. Here is a brief introduction to their usage methods one by one.

1. Thread

This is perhaps the most complex approach, but it provides a variety of flexible control over threads. First of all, you must use its constructor to create a thread instance. Its parameters are relatively simple, with only one ThreadStart delegate: public Thread(ThreadStart start); and then call Start() to start it. Of course, you can use its Priority property to set or obtain its running priority (enum ThreadPriority: Normal, Lowest, Highest, BelowNormal, AboveNormal).

The following example first generates two thread instances t1 and t2, then sets their priority respectively, and then starts the two threads (the two threads are basically the same, except that their output is different, t1 is "1" and t2 is "2". According to the ratio of their respective output characters, the ratio of their CPU time can be roughly seen, which also reflects their respective priority).

static void Main(string[] args)
{
 Thread t1 = new Thread(new ThreadStart(Thread1));
 Thread t2 = new Thread(new ThreadStart(Thread2));

  =  ;
  =  ;
   ();
  ();
 }
public static void Thread1()
{ 
 for (int i = 1; i &lt; 1000; i++) 
 {//Write a "1" for every loop running  dosth();
 ("1");
 }
 }
public static void Thread2()
{ 
 for (int i = 0; i &lt; 1000; i++) 
 {//Write a "2" for every loop running dosth();
 ("2");
 }
}
public static void dosth()
{// Used to simulate complex operations for (int j = 0; j &lt; 10000000; j++) 
 { 
 int a=15;
 a = a*a*a*a;
 }
}

The above program run results are:

11111111111111111111111111111111111111111121111111111111111111111111111111111111111112
11111111111111111111111111111111111111111121111111111111111111111111111111111111111112
11111111111111111111111111111111111111111121111111111111111111111111111111111111111112

From the above results, we can see that the t1 thread takes up much more CPU time than t2, because the priority of t1 is higher than that of t2. If we set the priority of t1 and t2 to Normal, the result is as follows:

121211221212121212121212121212121212121212121212121212121212121212121
212121212121212121212121212121212121212121212121212121212121212121212
121212121212121212

From the above example, we can see that its construction is similar to the worker thread of win32, but it is simpler. You just need to use the function to call the thread as a delegate, and then use the delegate as a parameter to construct the thread instance. When the call to Start() starts, the corresponding function will be called and the execution starts from the first line of the function.

Next, we combine the thread's ThreadState property to understand the thread's control. ThreadState is an enum type that reflects the state of the thread. When a Thread instance is just created, its ThreadState is Unstarted; when this thread is started by calling Start(), its ThreadState is Running; After this thread is started, if you want it to pause (block), you can call the() method. It has two overloaded methods (Sleep(int) and Sleep(Timespan)), which are just different formats of the amount of time. When this function is called in a thread, it means that the thread will be blocked for a period of time (time is determined by the number of milliseconds passed to Sleep or Timespan, but if the parameter is 0, it means that the thread is suspended to enable other threads to execute, specify Infinite Block threads indefinitely), at this time its ThreadState will become WaitSleepJoin. It is also worth noting that the Sleep() function is defined as static? ! This also means that it cannot be used in combination with a certain thread instance, that is, there is no call similar to (10)! That's true, the Sleep() function can only be called by the thread that needs "Sleep" itself, and other threads are not allowed to be called, just as when to Sleep is a personal matter that cannot be decided by others. However, when a thread is in the WaitSleepJoin state and has to wake it up, you can use the method, which will raise a ThreadInterruptedException on the thread. Let's take a look at an example first (note the call method of Sleep):

static void Main(string[] args)
{ 
 Thread t1 = new Thread(new ThreadStart(Thread1));
 ();
   ();
   ();
   ();
   ();
   (“t1 is end”);
}
static AutoResetEvent E = new AutoResetEvent(false);
public static void Thread1()
{ 
 try
 {// As can be seen from the parameters, it will cause sleep (); 
 }
 catch( e)
 {//Interrupt Handling Program  (" 1st interrupt");
 }
   ();
 try
 {// Sleep ( ); 
 }
 catch( e)
 {
   (" 2nd interrupt");
 }//Pause for 10 seconds    (10000); 
  }

The running result is:

1st interrupt
 2nd interrupt
(after 10s)t1 is end

From the above example, we can see that the method can wake up the program from a certain blocking state to the corresponding interrupt handler, and then continue to execute (its ThreadState also becomes Running). The following points must be paid attention to when using this function:

1. This method can not only wake up blockage caused by Sleep, but also be effective for all methods that can cause threads to enter WaitSleepJoin state (such as Wait and Join). As shown in the above example, when using it, put the method that causes thread blocking into the try block and put the corresponding interrupt handler into the catch block.

2. Call Interrupt on a thread. If it is in the WaitSleepJoin state, it will enter the corresponding interrupt handler execution. If it is not in the WaitSleepJoin state at this time, it will be interrupted immediately when it enters this state later. If Interrupt is called several times before the interrupt, only the first call is valid. This is exactly why I used synchronization in the above example, so that the second call Interrupt is called after the first interrupt. Otherwise, it may cause the second call to be invalid (if it is called before the first interrupt). You can try to remove the synchronization, and the result is likely to be: 1st interrupt

The above example also uses two other methods to make the thread enter the WaitSleepJoin state: using synchronous objects and methods. The use of the Join method is relatively simple. It means that the current thread calling this method is blocked until another thread (in this case t1) terminates or has passed the specified time (if it also contains the time parameter), when either of the two conditions (if any) appears, it immediately ends the WaitSleepJoin state and enters the Running state (you can judge what condition is based on the return value of the .Join method. If true, it means thread termination; false is the time). Thread pause also has methods. When a thread is in the Running state, it will enter the SuspendRequested state, but it will not be suspended immediately. It can not hang the thread until the thread reaches the safe point, and it will enter the Suspended state. If it is called on a thread that is already in Suspended, it is invalid. To resume running, just call it.

Finally, we are talking about thread destruction. We can call the Abort method on the thread that needs to be destroyed, and it will raise a ThreadAbortException on this thread. We can put some code in the thread into the try block and put the corresponding processing code into the corresponding catch block. When the thread is executing the code in the try block, if Abort is called, it will jump into the corresponding catch block for execution. After executing the code in the catch fast, it will terminate (if ResetAbort is executed in the catch block, it is different: it will cancel the current Abort request and continue to execute downward. Therefore, if you want to ensure that a certain thread terminates, the best way to join is to ensure that the terminating of a thread is as shown in the above example).

2. ThreadPool

ThreadPool is a relatively simple method that is suitable for short tasks that require multiple threads (such as some threads that are often blocked). Its disadvantage is that it cannot control the created threads and cannot set its priority. Since each process has only one thread pool, and of course each application domain also has one thread pool (wiring), you will find that the member functions of the ThreadPool class are static! When you call, etc. for the first time, a thread pool instance will be created. Here is an introduction to the two functions in the thread pool:

public static bool QueueUserWorkItem( //Return true if the call is successfulWaitCallback callBack,//Delegate of the thread to be created  object state // Parameters passed to the delegate)//Its other overload function is similar, it just has the delegate without parameters.

The function of this function is to queue the thread to be created to the thread pool. When the number of available threads in the thread pool is not zero (the thread pool has a limit on the number of threads created, and the missing value is 25), this thread is created. Otherwise, it will be queued to the thread pool until it has available threads.

public static RegisteredWaitHandle RegisterWaitForSingleObject(
 WaitHandle waitObject,// WaitHandle to register WaitOrTimerCallback callBack,// Delegation of thread call object state,// Parameters passed to the delegate int TimeOut,//Timeout, unit is milliseconds, bool executeOnlyOnce file://Is it only executed once); 
public delegate void WaitOrTimerCallback(
 object state,// That is, the parameters passed to the delegate bool timedOut//true means that the call is called due to timeout, otherwise it is because of waitObject);

The function of this function is to create a waiting thread. Once this function is called, it will create this thread. Before the parameter waitObject becomes the terminating state or the set time TimeOut arrives, it is in a "blocking" state. It is worth noting that this "blocking" is very different from the WaitSleepJoin state of Thread: when a Thread is in the WaitSleepJoin state, the CPU will periodically wake it up to poll and update the status information, and then enter the WaitSleepJoin state again. The thread switching is very resource-consuming; while the thread created with this function is different. Before it is triggered to run, the CPU will not switch to this thread. It neither takes up CPU time nor wastes thread switching time, but how does the CPU know when to run it? In fact, the thread pool will generate some helper threads to monitor these trigger conditions. Once the conditions are met, the corresponding threads will be started. Of course, these helper threads themselves also take up time, but if you need to create more waiting threads, the advantage of using thread pools becomes more obvious. See the following example:

static AutoResetEvent ev=new AutoResetEvent(false);
public static int Main(string[] args)
{ (
  ev,
  new WaitOrTimerCallback(WaitThreadFunc),
  4,
  2000,
  false// means that the timer is reset after each wait operation is completed and wait is cancelled until the waiting operation is cancelled  );
 (new WaitCallback (ThreadFunc),8);
 (10000);
 return 0;
}
 public static void ThreadFunc(object b)
{  ("the object is {0}",b);
for(int i=0;i&lt;2;i++)
{  (1000);
 ();
}
}
public static void WaitThreadFunc(object b,bool t)
{  ("the object is {0},t is {1}",b,t);
 }

The operation result is:

the object is 8
the object is 4,t is False
the object is 4,t is False
the object is 4,t is True
the object is 4,t is True
the object is 4,t is True

From the above results, we can see that the thread ThreadFunc has been run 1 time, while WaitThreadFunc has been run 5 times. We can judge the reason for starting this thread from the bool t parameter in WaitOrTimerCallback: t is false, which means it is due to waitObject, otherwise it is due to timeout. In addition, we can also pass some parameters to the thread through object b.

3、Timer

It works with methods that require periodic calls, it does not run in the thread that creates the timer, it runs in a separate thread that is automatically allocated by the system. This is similar to the SetTimer method in Win32. Its structure is:

public Timer(
 TimerCallback callback,//The method required to call object state,//The parameters passed to callback int dueTime,//How long does it take to start calling callback int period//The interval of calling this method); 

// If dueTime is 0, callback executes its first call immediately. If dueTime is Infinite, callback does not call its method. The timer is disabled, but it can be re-enabled using the Change method. If period is 0 or Infinite, and dueTime is not Infinite, callback calls its method once. The timer's periodic behavior is disabled, but it can be re-enabled using the Change method. If period is zero (0) or Infinite, and dueTime is not Infinite, callback calls its method once. The timer's periodic behavior is disabled, but it can be re-enabled using the Change method.

After creating the timer, if you want to change its period and dueTime, we can change it by calling the Timer's Change method:

public bool Change(
 int dueTime,
 int period
);//Obviously the two parameters changed correspond to the two parameters in Timerpublic static int Main(string[] args)
{ 
 ("period is 1000");
Timer tm=new Timer (new TimerCallback (TimerCall),3,1000,1000);
 (2000);
 ("period is 500");
 (0,800);
 (3000);
return 0;
 }
public static void TimerCall(object b)
{ 
 ("timercallback; b is {0}",b);
}

The operation result is:

period is 1000
timercallback;b is 3
timercallback;b is 3
period is 500
timercallback;b is 3
timercallback;b is 3
timercallback;b is 3
timercallback;b is 3 

Summarize

From the above brief introduction, we can see where they are used: Thread is suitable for situations where complex control of threads is required; ThreadPool is suitable for some tasks that require multiple threads and are shorter (such as some threads that are often blocked); Timer is suitable for methods that require periodic calls. As long as we understand their usage characteristics, we can choose the right method very well.

The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.