Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Threading & the .NET Framework


Aug01: Threading & the .NET Framework

Data Slots and Beta Software...


The .NET framework is Microsoft's answer to Java and the other technologies (such as Sun's Open Net Environment) for developing web-enabled applications. In the process, .NET offers a number of features aimed at simplifying previously difficult tasks. Classes that enable threading (that is, allowing a single process to contain multiple independent threads of execution) is one of those areas where the .NET framework offers the first real opportunity for many programmers to use an advanced feature of the operating system.

Unfortunately for programmers coming from Microsoft Visual Basic, Visual Basic for Applications, or VBScript, using multiple threads can be a little like running with scissors. Nothing bad happens most of the time, but then, when you least expect it — bang! A simple fall can have disastrous consequences. In this article, I'll examine what's involved in using threads and provide background on the problems that can occur.

Uses of Multiple Threads of Execution

Why might you want to use multiple threads of execution? There are two general classes of applications that benefit from using threads.

  • The first is a client application that needs to do some potentially slow processing while still allowing users of the application to continue working in the application. Printing a document or report while users continue editing is the classic example of this.

  • Server-based applications are the second example of apps that benefit from multiple threads. For instance, if there are two parts of a process, one relatively quick and the other relatively slow, it might make sense to have a single thread handle the relatively quick task, and dole out the results of that task to a pool of worker threads that will each perform the slower operation. A recent example in my experience is an application that reads data from a database, does some processing, and sends e-mail. Reading the data from the database is relatively fast, and doing the processing and sending the e-mail (with the potential for timeouts or errors) is relatively slow. A single thread can read the database and assign individual threads the task of processing the information and sending the e-mail for each request.

Potential Problems with Threaded Applications

If threads are so useful, why not use them all the time? First, it is possible that if you use enough threads, the application will spend more time switching context from thread to thread than doing the actual work of the application. Generally, machines running the .NET framework will have only a single processor. To give the appearance of running multiple threads (or applications) simultaneously, the processor must switch from one context to another quickly. So, look at the C# code in Listing One.

While you would expect each line to be executed in turn without interruption, it is, in fact, possible that some other thread or process could get to do some of its work between the creation of the array and the first initialization. In this example, it is not a problem for these operations to be interrupted.

However, imagine the slightly different scenario in Listing Two. In this case, you can still be sure that the value printed for i[3] will be 3. Now, taking on faith that the code created the threads, imagine the modification in Listing Three.

In this example, i[3] should always equal 3. But because of the way that this code is constructed, there is no guarantee that it will equal three. Since initializeIt() and readIt() are both operating independently, there is no assurance that initializeIt() will get to the assignment i[3] before the thread running readIt() prints out the value.

The situation would be even worse if both threads were writing the value simultaneously, rather than one thread writing the value and the other reading the value. Still more troublesome is the case of a complex type. For instance, given the structure in Listing Four, when assigning values to the individual char elements of an instance of the COLOR structure that is accessible from multiple threads, the COLOR structure could be read between the assignment of red and green. Thankfully, there are objects within the .NET framework that make this threading work cleanly.

.NET Framework Threading Concepts

There are several different problems with respect to controlling multiple threads. The first, which I described in the previous example, is control of access to shared resources. Other problems include coordinating operations between threads. For instance, a common model for creating a server application is to create multiple worker threads, then have those threads process requests as needed. This requires a different type of operation than merely serializing access to global variables.

If you are familiar with the existing Win32 threading concepts and functions, a cross-reference to the new .NET threading concepts and functions might be useful. Table 1 presents such a cross-reference.

The first thing that you should notice is that, as with the rest of the .NET framework, all operations are methods of objects, as opposed to the straight Win32 API calls. In general, the object method names are well thought out and reasonable. An exception is the oddly named Join method on the Thread object, which is used to allow one thread to wait for another thread to terminate.

A Threading Example

To understand .NET threading, let's look at a class that does C# thread pooling in Listing Five. This class currently uses the Sleep method of the Thread class to simulate work. There are 10 threads created, and there are then 100 calls to dispatch some work to a thread. The worker thread uses thread synchronization to set up sections in the thread that would allow data to be shared. Listing Six shows the same class using VB.NET.

Within the .NET framework, external functionality is made available in different ways than in other languages. In C++ for example, it is common to use some external library. You would #include a header file and link in a static library that either contained the functionality directly or called an external DLL. In Visual Basic, either supplying a prototype for an external function in a DLL or adding an external component to the References of a project included external functionality. In C#, the Using keyword allows external functionality to be referenced. And in VB.NET, the Imports keyword does approximately the same thing. The argument to either the Using or Imports command in their respective languages is a namespace that should be imported. In Listing Five, the similar namespaces System and System.Threading are used. This is the way that Java imports external functionality. But in the case of Java, ending the import with an .* will import all classes further down in the hierarchy. There is no similar "include everything below this" syntax in C# or VB.NET.

As with all C# programs, everything is part of a class, even the entry point to the program, called public static int Main(string[] args) in the ThreadTest class. One important factor of Main() being a static member of the class is that there are no instance variables available. In this example, when the Main() static method of ThreadTest is first entered, there is no instance of ThreadTest that has been created yet. The first thing that Main() does is create an instance of ThreadTest and declare a variable of type Thread.

Once the instance of ThreadTest is created, Listing Five enters a loop to create 10 threads, each using the WorkerThread() method of the ThreadTest instance just created as the thread entry point; see Listing Seven.

As mentioned in Table 1, creating a thread in the .NET framework is a two-step process (as opposed to the single-step CreateThread() in the Win32 API). First, the thread is created with the new keyword, and a newly created instance of ThreadStart is passed to the Thread constructor. The ThreadStart constructor has been passed the object method that you wish to use as the entry point for the new thread. In this example, I am not keeping track of each thread object as it is created. In the real world, the Thread object instances would be stored in an array so that they could later be controlled, and so that Main() could ensure that all threads are terminated before Main() is exited, effectively killing off any existing threads. A thread is not actually doing anything until Start() is called on the instance of the thread. This is truly just a method to start a thread. Once a thread is dead, it cannot be restarted using Start().

Once the for loop is exited, all 10 threads are active and have started to execute. In this case, because of the way that the WorkerThread() method is created, all the threads will wait as one of their first actions. If, instead, a thread needed to have some other action take place after creation but before execution, the call to the Start() method of the class could be delayed.

The Worker Thread

Before going any further in the Main() method, look at the WorkerThread() method, which is designed to do the actual work of the class. The name of the method is unimportant and could have been any legal method name, but I call it WorkerThread() for clarity. Other features of the method are quite important. In the Win32 world, if you want to use a class method as the entry point for a thread, the method must be a static method. Nonstatic methods of a C++ class (and, presumably, a C# class) have an unseen this argument that is passed to the method as a first parameter. The this pointer is used to allow the method to use instance data for the particular instance of the class it is called upon. The opposite is true for thread entry points: They must be nonstatic members of the class.

Further, unlike the Win32 ThreadProc that takes a single argument of a void pointer (a pointer to anything, really), the object method used as an entry point for .NET applications has no argument. Presumably, there is a way to pass context information to a thread entry point; but given the current state of the .NET framework documentation, it is not clear exactly how that is handled (see the accompanying text box entitled "Data Slots and Beta Software..."). For the WorkerThread() method in this ThreadTest class, the thread is designed to perform multiple loops over and over again with a different context each time, so the lack of a clear way to pass in context as the thread is started is not critical.

A consequence of this change in the entry point function is that new threads have a concept of this — meaning that instance variables are available within the thread. The availability of the instance data is useful, but of course, access to the instance data must be guarded. Looking at Listing Five, the first thing that the WorkerThread() method does is:

int th=0;

lock(cs)

{

threadNum++;

th=threadNum;

}

A local integer variable is declared (th) and the lock() statement is used to serialize access to a section of code. In this case, I am setting a variable local to WorkerThread() to the relative number of this thread. The lone argument to lock() is an object that lets you control the granularity of locking. For example, whenever one thread is within the block of code following the aforementioned lock() statement, no other thread can enter a block of code with the same object as the argument. However, if there was code such as the following elsewhere in the class:

lock(this)

{

threadNum++;

th=threadNum;

}

the block could be entered, since the block is locking on a different object. Doing this would be a bad idea, since the variables being manipulated here are the same as the variables being manipulated in the previous block locked on cs, but the ability to lock on different objects can be useful in other scenarios. Programmers will commonly use the this object for locking if they are only locking one set of variables. Always think carefully about the amount of work you do inside a synchronized section of code, as well as what level of locking granularity you need. For instance, if you always lock on the this object, the class will be serialized through these sections. That is, the code within the locked sections will only be executing one thread at a time. If large sections of code are locked in this way, the multithreaded application may perform no better than a single threaded application. In Listing Five, I use the cs object (declared and instantiated as an object) just to remind you how similar this is to the Critical Section from the Win32 API. The VB.NET equivalent of lock() is SyncLock.

What is really happening when lock() or SyncLock is called? Listing Eight is roughly equivalent to Listing Nine. Why might you ever use the underlying System.Threading.Monitor object? While lock() is convenient, there are times when the extended features available in the full object might be required. For instance, System.Threading.Monitor has a set of TryEnter and Wait methods that let a thread test to see if the Monitor can be entered without blocking, or with blocking for a finite period of time. When creating a server application, you need to be able to avoid locking forever so that the server can end based upon a user's command or some other external event.

Next in Listing Five, WorkerThread() enters a loop controlled by the value of a flag, bRunning, that tells the thread when it should end. Inside this loop, there is an if statement that uses the AutoResetEvent object (a member of the class); see Listing Ten.

One of the most misunderstood synchronization objects in Win32 (and likely to be misunderstood in .NET) is the event object. An "event" is exactly what you say it is, no more. So, within the Main() method of our example, you are in a loop and you set the event object using the Set() method. All this means is that you are signaling — telling any waiting threads, "Hello! I have just set this event!" I am not sure what would have been a better name than "event," but it adds to the confusion because folks presume that there is some tie to Windows events, such as a window opening.

In Win32, an event object is created using CreateEvent(), and whether it is a manual reset event or an automatic reset event, it is controlled by a flag to the create function. In .NET, there are two separate but similar objects — one called System.Threading.ManualResetEvent and the other System.Threading.AutoResetEvent. The difference is that the manual reset event remains signaled until it is reset manually. The auto reset event is automatically reset when a single waiting thread is released. One, and only one, thread will be released, even if multiple threads are waiting on the event.

The behavior of the AutoResetEvent is perfect for what Listing Five is trying to do — parcel out work to one, and only one, waiting thread. Thus, when the thread calls the WaitOne() method on the event object with a timeout of 1000 (1000 milliseconds or 1 second), if the main thread sets the event and this thread is released, it can be sure that it is the only worker thread being released. If the WaitOne() method returns False, then the main thread did not set the event in that one second, or some other threads were released by signaling. If no thread is waiting, then the event will remain signaled. In Listing Five, all I do is display a message on the console that lets you know if the thread was signaled or if it had to sleep. Note that Listing Six contains a VB.NET version of Listing Five.

What Else Is in .NET, and What Is Missing?

Thread pooling, introduced in Windows 2000, is available in .NET, and it has some of the same limitations as far as fine control over the number of threads in the pool. This has caused me to shy away from it. Virtually every threading primitive and API is somehow represented in .NET. Threads in .NET can also be either Foreground or Background threads. The difference between the two is that once only Background threads are running, the run time will kill them by throwing a ThreadAbortException. Background threads can be used to execute code that is useful only within the context of some other foreground operation that will be running on a Foreground thread. Once the last Foreground thread is terminated, the Background threads alone will not keep the runtime alive and will thus be terminated.

Given the rich object model available for threading, you might think that everything from Win32 is in .NET. That is true, with a few exceptions. There is still some uncertainty as to how context can be passed to threads, but I believe that this is just a documentation oversight. There is another minor omission as well: the lack of a spin count equivalent. In Win32, for multiprocessor machines, you can set a spin count to allow a thread waiting on a critical section to briefly delay (some time controlled by the spin count) if a critical section is owned by another thread. This is done in the hope that the critical section will be available after the brief delay, saving a relatively expensive trip to access the underlying kernel object. This is not a terrible omission, and in any event, it can be replaced by developers if required.

DDJ

Listing One

Int[] i = new int[4];
i[0]=0;
i[1]=1;
i[2]=2;
i[3]=3;

Back to Article

Listing Two

namespace ThreadingArticle
{
    using System;
    public class Class1
    {
        int[] i;
        public Class1()
        {
           i = new int[4];
        }
        public static int Main(string[] args)
        {
           Class1 c = new Class1();
           c.initializeIt();
           c.readIt()
           return 0;
        }
      public void readIt()
      {
        System.Console.WriteLine("i[3]={0}",i[3]);
      }   
      public void initializeIt()
      {
        i[0]=0;
        i[1]=1;
        i[2]=2;
        i[3]=3;
      }
    }
}

Back to Article

Listing Three

namespace ThreadingArticle
{
   using System;
   using System.Threading;
   public class Class1
   {
      int[] i;
      public Class1()
      {
         i = new int[4];
      }
      public static int Main(string[] args)
      {
            Class1 c = new Class1();
            Thread t = null;
            // Create a thread to run initializeIt()
            t = new 
              Thread(new ThreadStart(c.initializeIt));
            t.Start();
            // Create a thread to run readIt()
            t = new 
              Thread(new ThreadStart(c.readIt));
            t.Start();
            return 0;
        }
        public void readIt()
        {
            System.Console.WriteLine("i[3]={0}",i[3]);
        }
        public void initializeIt()
        {
            i[0]=0;
            i[1]=1;
            i[2]=2;
            i[3]=3;
        }
    }
}

Back to Article

Listing Four

Struct COLOR {
    char red;
    char green;
    char blue;
}

Back to Article

Listing Five

namespace ThreadTest
{
    using System;
    using System.Threading;

    public class ThreadTest
    {
        public System.Threading.AutoResetEvent hEvent;
        // Object to lock() on.
        public Object cs;
        public int threadNum;
        public bool bRunning;
        public ThreadTest()
        {
            bRunning=true;
            hEvent=new System.Threading.AutoResetEvent(false);
            cs=new Object();
            threadNum=0;
     }
     public void WorkerThread() 
     {
        int th=0;
        lock(cs)
        {
            threadNum++;
            th=threadNum;
        }
        while ( bRunning==true )
        {
            if (hEvent.WaitOne(1000,false)==true)
            {
                lock(cs)
                {
                  System.Console.WriteLine("Thread {0} Got Handle, 
                                 and could safely get variable",th);
                }
            }
            else
            {
                Thread.Sleep(1000);
                System.Console.WriteLine("Thread {0} Slept...",th);
            }
        }   // End of while (bRunning==true)
    }   // End of WorkerThread()
    public static int Main(string[] args)
    {
    // We use this to get an object reference to this method. In the 
    // olden days, we would use a static method, because non-static methods
    // have magic, invisible "this" pointers.  
    ThreadTest b=new ThreadTest();
    Thread t=null;
    // If this was doing work we cared about, would have an array of Thread
    // objects, and then use .Join() method to ensure they had exited...
    for (int loop=0 ; loop < 10 ; loop++)
    {
      t = new Thread(new ThreadStart(b.WorkerThread));
      t.Start();
    }
    for (int loop=0;loop<100;loop++)
    {
        Random r = new Random();
        // Just to make it seem like some work was happening...
        Thread.Sleep(r.Next(1,200));
    
        // We lock on cs.  The WorkerThread() will, after returning 
        // successfully from wait, try to acquire lock as well. I will 
        // not try and set event again until thread is done with that 
        // section, presumably, after having read some data.
        lock(b.cs)
        {
            // Muck with data that is shared, and then set event...
            //  should check if this returns true or false...
            b.hEvent.Set();
         }
      }
    b.bRunning=false;
            return 0;
       } // End of Main()
}  // End of Namespace

Back to Article

Listing Six

Imports System.Threading
Module Module1
    Sub Main()
        Dim b As New ThreadTestVB()
        Dim t As Thread
        Dim tloop As Integer
        Dim r As Random
        Dim ts As ThreadStart
        
        t = Nothing
        For tloop = 1 To 10
            ts = New ThreadStart(AddressOf b.WorkerThread)
            t = New Thread(ts)
            t.Start()
        Next
        r = New Random()
        For tloop = 1 To 100
            Thread.Sleep(r.Next(1, 200))
            SyncLock (b.cs)
                b.hEvent.Set()
            End SyncLock
        Next
    End Sub
    Public Class ThreadTestVB
        Public Dim hEvent As AutoResetEvent
        Public Dim cs As Object
        Dim threadNum As Integer
        Dim bRunning As Boolean
        
        Public Sub New()
            bRunning = True
            hEvent = New AutoResetEvent(False)
            cs = New Object()
            threadNum = 0
        End Sub
        
        Public Sub WorkerThread()
            Dim th As Integer
            SyncLock (cs)
                threadNum = threadNum + 1
                th = threadNum
            End SyncLock
            While bRunning = True
                If hEvent.WaitOne(1000, False) = True Then
                    SyncLock (cs)
                        System.Console.WriteLine( _
        "Thread {0} Got Handle and could safely get variable", _th)
                    End SyncLock
                Else
                    Thread.Sleep(1000)
                    system.Console.WriteLine( _"Thread {0} Slept", th)
                End If
            End While
        End Sub
    End Class
End Module

Back to Article

Listing Seven

for ( int loop=0 ;loop<10 ; loop++)
{
    t = new Thread(new ThreadStart(b.WorkerThread));
    t.Start();
}

Back to Article

Listing Eight

System.Threading.Monitor.Enter(this);
try
{
    // code here
}
finally
{
    System.Threading.Monitor.Exit(this);
}

Back to Article

Listing Nine

lock()
{
    // code here
}

Back to Article

Listing Ten

if (hEvent.WaitOne(1000,false)==true)
{
lock(cs)
    {
        System.Console.WriteLine(
    "Thread {0} Got Handle, and could safely get variable", th);
    }
}
else
{
    Thread.Sleep(1000);
    System.Console.WriteLine("Thread {0} Slept...",th);
}

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.