CLR Threading Support

A single thread in managed code can be represented by one of two classes:

  • System.Threading.Thread represents a managed thread.

  • System.Diagnostics.ProcessThread represents an Operating System thread.

The reason for having two different classes is that the concepts of managed thread and OS thread are not the same. A managed thread won't necessarily correspond to a thread in the Windows operating system. In .NET version 1, a managed thread is generally simply an operating system thread that happens to be executing managed code, and which the CLR knows about and therefore maintains some extra data structures in connection with. (For example, the CLR can give textual names to threads, something that Windows doesn't natively support.) However, it is possible that in future Microsoft may choose to implement CLR threads differently, for example using some technique based on fibers. It may happen in future that the CLR will maintain its own logical threads, but under the hood each logical thread will swap between different physical threads according to which threads are available. Although this sounds potentially complex, it could have benefits - for example, it may mean that the CLR is able to use fewer system threads, while giving the impression to your code that more threads are available to it. I stress that there is no guarantee that Microsoft will go down this path. However, they have made sure that the option is available in case it is deemed useful in the future.

Because of this, the framework base classes have been defined in a way that prevents you from directly identifying a CLR managed thread with an OS thread. One consequence of this is that no cast exists that allows you to convert between Thread and ProcessThread.

For most of this chapter, we will be focussing on Thread. My aim is to show you how best to take advantage of the threading facilities offered by the CLR, and directly accessing OS threads simply isn't relevant to that. So you should bear in mind that when I talk about threads in this chapter, I am normally referring to logical, managed, CLR threads unless I explicitly say otherwise. However, we will briefly look at how to use ProcessThread to access information about OS threads, in case you are in a situation where you need to do so, for example because of compatibility requirements when working with unmanaged code.

The difference between managed and unmanaged threads can easily be seen simply by writing a managed "Hello World" console application, running it, and using Task Manager to examine the threads in it. You might have imagined that the following code uses only one thread:

 // HelloWorld Project void Main() {    Console.WriteLine("Hello, World!");    // The ReadLine() effectively pauses the program so we can use Task    // Manager to examine it    Console.ReadLine(); } 

In fact, on my main work computer (a single-processor Athlon machine) this code has three unmanaged threads in it when a release build is run from the command line (without VS.NET):

click to expand

If you try doing the same test, you may get different results, since the exact number of threads seems to vary between machines and operating systems. You may also have more threads if you run a debug build.

Bluntly, any managed application is multi-threaded as far as Windows is concerned, even if you think you are dealing with only one thread. In the above screenshot, one of the threads is clearly the one that's executing the Main() method. Although it's not documented what the other threads are for, we can hazard quite a good guess. One of them is likely to be the dedicated thread that destructors are executed on, which we discussed in Chapter 5, and the other will have something to do with the internal operation of the CLR.

Types of Managed Thread

As far as the managed threads that your code has access to are concerned, any threads that are to do with the internal operation of the CLR may as well not exist. Your code only has access to those managed threads whose purpose is to execute your code. This means the thread that the Main() method starts executing on, and any other threads that are explicitly or implicitly created by your code (plus, for finalizers, the dedicated finalizer thread).

The threads that are visible to your code fall into two categories:

  • Thread-Pool Threads comprise the thread on which your entry point method starts executing and any threads that are created are created behind the scenes by the CLR to perform such tasks as asynchronous processing when you request that a method be executed asynchronously.

  • Non-Thread-Pool Threads are created explicitly in your code using new Thread(ThreadStart entryPoint).

Thread-Pool Threads

You're probably familiar with the concept of a thread pool from unmanaged code. The idea is simply that there are a number of threads (a pool of threads) that are normally sleeping but that are available to do work. When some task needs to be done in the background, instead of your code instantiating a thread just for that task, one of these threads is woken up and handed the task to do. When it's complete, that thread goes back to sleep again. Having a thread pool is potentially a very efficient way of doing things, since waking up an existing thread is a lot quicker than explicitly creating a new thread (which involves Windows allocating all the related data structures for that thread). A thread pool means that the number of threads is kept within reasonable limits while at the same time giving your program freedom to perform extensive multitasking. If too many tasks come along at once, then instead of overloading the CPU by trying to run them all at the same time, some of the tasks have to wait until a thread in the pool becomes available. The problem with thread pools in unmanaged code has of course always been that you have to write the infrastructure to implement the pool yourself, and that is a major programming task (although Windows 2000 did introduce thread-pool support). For this reason, thread pools are rarely used in unmanaged code, except in a few cases where Microsoft had supplied specific APIs that implemented them internally (such as OLE DB and MTS/COM+). In .NET by contrast, there is in-built support for a thread pool. There is limited direct access to the thread pool via the System.Threading.ThreadPool class.

One of the biggest differences between multi-threading in managed and unmanaged code is arguably the way that the CLR makes it easy (and preferable) to use the thread pool to accomplish tasks that in unmanaged code would usually have been done by explicitly creating threads. This does mean there is a considerable shift in typical threading design of a multi-threaded application for managed code, which you'll need to get used to if you're used to coding up unmanaged multi-threaded applications.

Although there are a couple of useful static methods on ThreadPool to give you information about the pool, you can't create thread pool threads explicitly, nor is there any general mechanism to get a Thread reference to a thread pool thread. The idea is that you let the CLR handle the thread pool threads, which gives better performance but means you can't manipulate the threads yourself. In particular, if you ask the CLR to execute some task asynchronously, then it's up to the CLR which thread it picks from the pool to execute it on; you have no control over that.

You can find out how many threads can be placed in the thread pool using the static ThreadPool.GetMaxThreads() method:

 int maxWorkerThreads; int maxCompletionPortThreads; ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxCompletionPortThreads); 

On my processor this method tells me the thread pool can contain 25 of each type of thread. Note that I say 'can contain'. These threads won't actually be created unless they are required - it would be silly to have 50 threads permanently around when most applications will never need that many, and some applications won't use the thread pool at all. This number may change on different computers or different versions of .NET, and it's likely to be larger on multi-processor machines. A similar method, ThreadPool.GetAvailableThreads() can tell you how many more asynchronous tasks can be simultaneously executed before the thread pool is full.

The method has two output parameters to retrieve the maximum number of two different kinds of thread-pool threads. We'll look at worker threads shortly; completion port threads are special threads for asynchronous I/O operations.

You can also explicitly ask a task to be executed asynchronously on a thread-pool thread, using the static ThreadPool.QueueUserWorkItem() method. This method takes a delegate of type System.Threading.WaitCallback that represents the method to be executed:

 // Assume that void DoSomeWork(object state) is the entry method for // the task to be performed WaitCallback task = new WaitCallback(DoSomeWork) ThreadPool.QueueUserWorkItem(task, state); 

If no thread-pool thread is currently available to perform the task, the task will wait in a queue until a thread is available, hence the name QueueUserWorkItem(). However, although this method is worth bearing in mind, we won't be using it in the samples in this chapter, since there are other more powerful techniques and concepts available that we need to cover.

Non-Thread-Pool Threads

You will normally instantiate a non-thread-pool thread if for some reason you want to have explicit control over what the thread is doing and this requires you to have an explicit reference to that thread.

 ThreadStart entryPointDelegate = new ThreadStart(EntryPoint); Thread workerThread = new Thread(entryPointDelegate); workerThread.Start(); // Later... void EntryPoint() // This is the method at which thread starts execution { 

The above code snippet shows how you can explicitly create a thread: you first set up a delegate of type System.Threading.ThreadStart, which should refer to the method at which the new thread will start executing. You then instantiate a Thread object, passing it this delegate as a parameter. The thread starts running when you call its Thread.Start() method, and normally terminates when its execution flow returns from the method at which execution started.

The important point about above code is that it leaves the first thread in possession of the Thread reference that represents the new thread. This means the main thread is able to invoke various methods and properties on this object to find out information about the new thread, as well as perform actions such as terminating the thread. You lose the benefits of having a thread pool, but you do get finer control over the thread you've created.

I won't list all the Thread methods and properties here, as I'm more interested in getting across the basic principles. You're more than capable of looking up the list of methods in the MSDN docs! However, you will see various Thread methods in action as we work through the chapter.

I will mention, though, that if you need a reference to a Thread object that represents the thread your code is currently executing on, you can access it through the Thread.CurrentThread static property:

 Thread myOwnThread = Thread.CurrentThread; 

Other Categories of Threads

There are couple of other ways we can categorize threads.

The CLR introduced the concept of background threads; these are threads that have no power to hold a process open. The initial thread that starts running your code is a foreground thread, which means that as long as that thread exists, so will the process. When you create a new thread, it defaults to being foreground too, but you can explicitly mark it as a background thread:

 Thread.CurrentThread.IsBackground = true; 

Windows will terminate a process when there are no more foreground threads in that process - even if there are background threads running. In most cases, if you are creating your own threads it's probably better to leave them as a foreground thread, so that they can explicitly clean up any data they are using before terminating. Thread-pool threads, however, are all background threads - this makes sense since thread-pool threads will tend to spend most of their time waiting for tasks, and are not under your control.

A user-interface thread is a thread that is executing a message loop. A message loop is a continuous loop within an application that processes messages sent by Windows (for example, telling the application to repaint itself, or to quit). In practice, this means that a user-interface thread is not continually executing code. Instead, it sleeps most of the time, waking up whenever some message needs to be processed.

We'll look at message loops in more detail in , when we examine Windows Forms.

In contrast, threads that are simply executing code not inside a message loop (such as the Main() method of a console application) are referred to as worker threads. The terms user-interface thread and worker thread arose because of the traditional architecture of Windows applications. The main thread in a Windows Forms application will normally have a message loop, which is used to process user input. This main thread may instantiate other threads to do certain work - and these other threads traditionally don't have message loops. If you call Application.Run() on a thread, that thread will by definition become a user interface thread, since Application.Run() works internally by starting a message loop. However, a thread doesn't have to be processing a user interface in order to have a message loop. For example, COM STA threads have message loops but no user interface - so to that extent the term is a bit misleading.

The distinction between a UI and a worker thread is purely a terminological one (and the terminology applies equally to unmanaged and managed code). There is no formal difference recognized by Windows, hence there are no properties of Thread to determine which category a given thread falls into.

Thread Identity

Unmanaged threads are normally identified by a thread ID - a number that maps to an OS handle that identifies the thread's resources. However, the concept of a thread ID is not defined for managed threads, which will be identified by a hash code, or by a name. The hash code is always present, but the name will be blank unless it is explicitly set by your code.

 Thread thisThread = Thread.CurrentThread; int hash = thisThread.GetHashCode(); thisThread.Name = "My nice thread"; 

Note that you can only set the name once - once set, you cannot change it.

Although you can use the hash code to identify a thread, it's not generally possible to obtain a Thread reference given its hash code.

If you do specifically need to know the thread ID of the current running thread, then you can use the static System.AppDomain.GetCurrentThreadId() method.

 int id = AppDomain.GetCurrentThreadId(); 

Generally speaking, you'll only want to do this if you need to pass the ID to some native code. Bearing in mind the possibility that physical and logical threads may become separated in future versions of .NET, you should probably avoid caching this value across any points where the CLR takes control over the flow of execution.

Enumerating Unmanaged Threads

It's possible to enumerate over the unmanaged threads in a process using the static System.Diagnostics.Process.Threads property. This yields a collection of ProcessThread references. We will show you how to do this for completeness, but in terms of managed code there's very little you can do with the ProcessThread references. The most common reason for using this technique is likely to be to obtain the thread IDs (which are available through the ProcessThread.Id property) to pass to some unmanaged code. The fact that the ProcessThread is in the System.Diagnostics namespace gives a pretty good clue as to the intended purpose of the class: it's there to help with debugging, or to perform detailed diagnostic analysis of a process - it's not intended for normal everyday use.

The following sample enumerates the threads in the process, displaying the ID, thread state, and priority level of each:

 static void Main() {    ProcessThreadCollection ptc = Process.GetCurrentProcess().Threads;     Console.WriteLine("{0} threads in process", ptc.Count);    foreach (ProcessThread pt in ptc)    {       Console.WriteLine("ID; {0}, State: {1}, Priority: {2}", pt.Id,                         pt.ThreadState, pt.PriorityLevel);    } } 

The code gives this output on my machine in release mode (you may get different numbers of threads on different machines, and also the results will be different in debug mode as there may be additional threads present for debugging purposes):

 7 threads in process ID: 3360, State: Running, Priority: Normal ID: 3364, State: Wait, Priority: Normal ID: 1032, State: Wait, Priority: Highest ID: 3356, State: Wait, Priority: Normal ID: 3336, State: Wait, Priority: Normal ID: 3372, State: Wait, Priority: Normal ID: 3348, State: Wait, Priority: Normal 

The ThreadState and PriorityLevel properties are respectively instances of the System.Diagnostics.ThreadState and System.Diagnostics.ThreadPriorityLevel enums. Although I've displayed these values for ProcessThread, you can get this data for the Thread class as well - you don't need to go through ProcessThread. However, note that the Thread class's ThreadState and Priority properties have slightly different enums to the ProcessThread equivalents - System.Threading.ThreadState and System.Threading.ThreadPriority respectively. The state indicates what the thread is currently doing (for example, whether it is sleeping, running, or waiting for a synchronization object).

By the way, although I've shown this sample just to give you an idea of the ProcessThread class, I don't suggest you use this technique to enumerate the physical threads in a real application. The reason is that you'll notice seven threads have been listed, whereas the earlier "HelloWorld" program only had three threads. There's a Heisenberg principle coming in here: enumerating the process threads actually causes more threads to be created by the CLR in order to perform the enumeration!



Advanced  .NET Programming
Advanced .NET Programming
ISBN: 1861006292
EAN: 2147483647
Year: 2002
Pages: 124

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net