CLR thread pool released on: 1/14/2005 | Updated on: 1/14/2005
Jeffrey Richter
Microsoft has been trying to improve its platform and ApplicationProgramPerformance. Many years ago, Microsoft looked at how application developers use threads to see what they can do to improve their utility. This study has an important discovery: developers often create new threads to execute a task. When the task is completed, the thread terminates.
This mode is extremely common in server applications. The client requests the server. The server creates a thread to process client requests. When the client requests are completed, the thread of the server terminates. Compared with processes, creating and destroying threads is faster, and fewer operating system resources are used. However, creating and destroying threads is not free of charge.
To create a thread, You need to allocate and initialize a kernel object and allocate and initialize the stack space of the thread. In addition, Windows sends a dll_thread_attach notification to each DLL in the process, allocate pages in the disk to the memory, and then executeCode. When the thread ends, a dll_thread_detach notification is sent to each DLL. The stack space of the thread is released and the kernel object is released (if the number of threads reaches 0 ). Therefore, many of the overhead related to thread creation and destruction are irrelevant to the work originally executed by the creation thread.
Thread Pool generation
This study prompted Microsoft to implement the thread pool, which first appeared in Windows 2000. When Microsoft. NET Framework Team designs and builds a Common Language Runtime Library (CLR), they decide to implement the thread pool in the CLR itself. In this way, even if the application runs in a Windows version earlier than Windows 2000 (such as Windows 98), any managed application can exploit the thread pool.
During CLR initialization, the thread pool does not contain threads. When an application needs to create a thread to execute the task, the application should request the thread pool thread to execute the task. An initial thread will be created after the thread pool is known. The initialization of the new thread is the same as that of other threads. However, after the task is completed, the thread will not be destroyed by itself. Instead, it returns the thread pool in the suspended state. If the application sends a request to the thread pool again, the suspended thread activates and executes the task without creating a new thread. This saves a lot of expenses. As long as the queuing speed of application tasks in the thread pool is lower than the speed at which one thread processes each task, the same thread can be reused repeatedly to save a lot of overhead during the lifetime of the application.
Then, if the application task queuing speed in the thread pool exceeds the speed at which one thread processes the task, an additional thread will be created in the thread pool. Of course, creating a new thread will produce additional overhead, but during its lifetime, the application may only request several threads to process all the tasks assigned to it. Therefore, in general, the performance of applications can be improved by using the thread pool.
Now you may want to know what will happen if the thread pool contains many threads and the application's workload is decreasing. In this case, the thread pool contains several threads that have been suspended for a long time, wasting operating system resources. Microsoft has also considered this issue. When the thread pool thread hangs, it waits for 40 seconds. If the thread has nothing to do after 40 seconds, the thread will be activated and destroyed by itself, releasing all the operating system resources (stacks, kernel objects, and so on) it uses ). At the same time, activation and self-destruction of threads may not affect the performance of the application, because the application does not do much, otherwise it will resume execution of this thread. By the way, although the threads in the thread pool are activated by themselves within 40 seconds, the actual time is not verified and can be changed.
A wonderful feature of the thread pool is that it is heuristic. If your application needs to execute many tasks, more threads will be created in the thread pool. If the workload of your application is gradually reduced, the thread pool thread will terminate on its own. Thread PoolAlgorithmMake sure that it only contains the number of threads required for the workload on it!
Therefore, we hope that you have understood the basic concepts of the thread pool and the performance advantages it can provide. Now I will provide some code to illustrate how to use the thread pool. First, you should know that the Thread Pool provides four functions:
• |
Asynchronous call Method |
• |
Call a method at a certain interval |
• |
Call method when a single kernel object receives a signal notification |
• |
Call method when the asynchronous I/O Request ends |
The first three functions are very useful. I will explain them in this column. Application developers seldom use the fourth feature, so I will not describe it here; it is possible to talk about it in future columns.
Function 1: asynchronous call Method
In your application, if you have a new thread to execute the task code, I suggest you replace it with the new code for executing the task in the Command thread pool. In fact, you usually find that it is easier to run tasks in the thread pool than to run tasks in a new dedicated thread.
To queue thread pool tasks, you can use the threadpool class defined in the system. Threading namespace. The threadpool class only provides static methods and cannot construct its instances. To allow the thread pool thread to call the method asynchronously, your code must call the overload queueuserworkitem method of a threadpool, as shown below:
Public static Boolean queueuserworkitem (waitcallback WC, object state); public static Boolean queueuserworkitem (waitcallback WC );
These methods queue "work items" (and optional State data) to the thread in the thread pool and return immediately. A work item is only a method (identified by the WC parameter) that is called and passed to a single parameter, that is, status data ). The queueuserworkitem version without the status parameter passes null to the callback method. Finally, some threads in the pool will call your method to process the work item. The callback method you write must match the system. Threading. waitcallback delegate type. Its definition is as follows:
Public Delegate void waitcallback (Object State );
Note that you should never call any method that allows you to create a thread by yourself. If necessary, the CLR thread pool will automatically create a thread and, if possible, reuse the existing thread. In addition, the thread will not be destroyed immediately after processing the callback method; it will return to the thread pool and prepare to process other work items in the queue. Using queueuserworkitem will make your application more effective, because you do not need to create or destroy threads for each client request.
Figure 1The code in shows how to make the thread pool call a method asynchronously.
Function 2: Call A method at a certain interval
If your application needs to execute a task at a certain time, or your application needs to periodically execute some methods, using the thread pool is your best choice. The system. Threading namespace defines the Timer class. When you construct an instance of the Timer class, you are telling the thread pool that you want to call back a method at a specific time in the future. The timer class has four constructor types:
Public timer (timercallback callback, object state, int32 duetime, int32 period); Public timer (timercallback callback, object state, uint32 duetime, uint32 period); Public timer (timercallback callback, object state, int64 duetime, int64 period); Public timer (timercallback callback, object state, timespan duetime, timespan period );
All four constructors construct identical timer objects. The callback parameter identifies the method that you want to call back by the thread pool. Of course, the callback method you write must match the system. Threading. timercallback delegate type. Its definition is as follows:
Public Delegate void timercallback (Object State );
The status parameter of the constructor allows you to pass the status data to the callback method. If there is no status data to be passed, you can pass null. The duetime parameter can be used to tell the thread pool how many milliseconds it will wait before calling your callback method for the first time. You can use a signed or unsigned 32-bit value, a signed 64-bit value, or a timespan value to specify the number of milliseconds. If you want to call the callback method immediately, set the duetime parameter to 0. The last parameter period allows you to specify the time to wait before each successive call, in milliseconds. If you pass 0 to this parameter, the thread pool will only call this callback method once.
After the timer object is constructed, the thread pool knows what to do and automatically monitors the time for you. However, the Timer class also provides several other methods that allow you to communicate with the thread pool to change when (or whether) the callback method should be used. Specifically, the Timer class provides several change and dispose methods:
Public Boolean change (int32 duetime, int32 period); Public Boolean change (uint32 duetime, uint32 period); Public Boolean change (int64 duetime, int64 period); Public Boolean change (timespan duetime, timespan period); Public Boolean dispose (waithandle policyobject );
The change method allows you to change the duetime and period of the timer object. The dispose method allows you to completely cancel the callback when all pending Callbacks are completed, and can use signals to notify kernel objects identified by the yyobject parameter.
Figure 2The code in shows how to let the thread pool thread call a method immediately and call it again every 2000 milliseconds (or two seconds.
Function 3: Call A method when a single kernel object receives a signal notification
Microsoft researchers found that many applications generate threads to wait for a single kernel object to receive a signal. Once the object receives a signal notification, this thread sends a notification to another thread, and then loops back, waiting for the object to send a signal again. Some developers even write several threads in the code, and each thread is waiting for an object. This is a huge waste of system resources. Therefore, if multiple threads in your application are waiting for a single kernel object to receive a signal notification, the thread pool will still be the best resource for you to improve application performance.
To enable the thread pool thread to call your callback method when the kernel object receives a signal notification, you can use some static methods defined in the system. Threading. threadpool class again. To enable the thread pool thread to call the method when the kernel object receives a signal notification, your code must call an overloaded registerwaithandle method. For more information, seeFigure 3.
When you call these methods, the H parameter identifies the kernel object you want the thread pool to wait. Because this parameter is an abstract base class system. Threading. waithandle, you can specify any class derived from this base class. In particular, you can pass a reference to autoresetevent, manualresetevent, or mutex object. The second parameter callback identifies the method that you want the thread pool thread to call. The callback method you implement must match the system. Threading. waitortimercallback delegate type. Its definition is shown in the following code line:
Public Delegate void waitortimercallback (object state, Boolean timedout );
The third parameter State allows you to specify certain State data that should be passed to the callback method. If there is no special State data to be passed, null is passed. The fourth parameter milliseconds allows you to tell the thread pool kernel objects the time they should wait before being notified. Here, we usually pass-1 to indicate infinite timeout. If the last parameter executeonlyonce is true, the thread pool thread will only execute the callback method once. However, if executeonlyonce is false, the thread pool thread will execute the callback method every time the kernel object receives a signal notification. This is very useful for autoresetevent objects.
When a callback method is called, the status data and Boolean value timedout are passed to it. If timedout is false, the method knows that it is called because the kernel object is notified by a signal. If timedout is true, the method knows that it is called because the kernel object is not notified by signal within the specified time. The callback method should perform all required operations.
In the prototype shown above, you will notice that the registerwaitforsingleobject method returns a registeredwaithandle object. This object determines the kernel object waiting for the thread pool. If, for some reason, your application needs to tell the thread pool to stop monitoring registered wait handles, then your application can call the registeredwaithandle's unregister method:
Public Boolean unregister (waithandle waitobject );
The waitobject parameter indicates how you want to receive a signal notification after all the work items in the queue are executed. If you do not want to receive a signal notification, you should pass null to this parameter. If you pass a valid reference to the waithandle-derived object, the thread pool will notify the object after all pending work items of the pending handle have been registered.
Figure 4The code in shows how to call a method when the thread pool thread receives a signal notification from the kernel object.
Summary
In this column, I talked about the need for a thread pool and explained how to use the various functions provided by the CLR thread pool. Now you should understand the value of the thread pool for your development. It can improve the performance of your application and simplify your code.
Code 1, 2, 3, and 4
Figure 1 thread pool cils a Method
Using system;
Using system. Threading;
Class app {
Static void main (){
Console. writeline ("main thread: queuing an aynchronous
Operation .");
Threadpool. queueuserworkitem (New waitcallback (myasyncoperation ));
Console. writeline ("main thread: Deming other operations .");
//...
console. writeline ("main thread: pausing to simulate doing other
operations. ");
console. readline ();
}
// The callback method's signature must match that of
// System. Threading. waitcallback delegate (it takes
// Object parameter and returns void)
Static void myasyncoperation (object state ){
Console. writeline ("threadpool thread: Deming aynchronous
Operation .");
//...
Thread. Sleep (5000); // sleep for 5 seconds to simulate doing
// Work
// Returning from this method causes the thread
// Suspend itself waiting for another task
}
}
--------------------------------------------------------------------------------
Figure 2 using the period Parameter
Using system;
Using system. Threading;
Class app {
Static void main (){
Console. writeline ("checking for status updates every 2 seconds .");
Console. writeline ("(hit enter to terminate the sample )");
Timer timer = new timer (New timercallback (checkstatus), null, 0,
2000 );
Console. Readline ();
}
// The callback method's signature must match that of
// System. Threading. timercallback delegate (it takes
// Object parameter and returns void)
Static void checkstatus (object state ){
Console. writeline ("Checking Status .");
//...
}
}
--------------------------------------------------------------------------------
Figure 3 registerwaithandle Methods
Public static registerwaithandle registerwaitforsingleobject (
Waithandle H, waitortimercallback callback, object state,
Uint32 milliseconds, Boolean executeonlyonce );
Public static registerwaithandle registerwaitforsingleobject (
Waithandle H, waitortimercallback callback, object state,
Int32 milliseconds, Boolean executeonlyonce );
Public static registerwaithandle registerwaitforsingleobject (
Waithandle H, waitortimercallback callback, object state,
Timespan milliseconds, Boolean executeonlyonce );
Public static registerwaithandle registerwaitforsingleobject (
Waithandle H, waitortimercallback callback, object state,
Int64 milliseconds, Boolean executeonlyonce );
--------------------------------------------------------------------------------
Figure 4 method called when object signaled
Using system;
Using system. Threading;
Class app {
Static void main (){
Autoresetevent are = new autoresetevent (false );
Registeredwaithandle rwh = threadpool. registerwaitforsingleobject (
Are, new waitortimercallback (eventsignalled), null,-1, false );
For (int32 x = 0; x <5; X ++ ){
Thread. Sleep (5000 );
Are. Set ();
}
Rwh. unregister (null );
Console. writeline ("hit enter to terminate the sample ");
Console. Readline ();
}
// The callback method's signature must match that of
// System. Threading. waitortimercallback delegate (it takes
// Object parameter and a Boolean and returns void)
Static void eventsignalled (object state, Boolean timedout ){
If (timedout ){
Console. writeline ("timed-out while waiting for
Autoresetevent .");
} Else {
Console. writeline ("The autoresetevent became signalled .");
}
//...
}
}