MTA Threading Model

 < Day Day Up > 



The biggest difference between an STA and an MTA threaded apartment is that an MTA apartment can have more than one thread running simultaneously in the same apartment using all the shared data available in the apartment. This is illustrated in Figure 2.

click to expand
Figure 2

Since the MTA model supports simultaneous multiple thread execution, it becomes the caller's responsibility to synchronize the global data between multiple threads. Many of these issues were covered in the previous chapter.

Specifying the Threading Model

The threading model for a thread can be set using the ApartmentState property of the Thread class. The ApartmentState enumeration defines the types of threading models supported by .NET.

Enumeration Value

Meaning

MTA

Creates a multi-threaded apartment

STA

Creates a single-threaded apartment

Unknown

The apartment property of the Thread class is not set

As we've already learned, you should only mark the thread as STA thread if you are going to access an STA-threaded legacy COM component. Otherwise, your threading model is in the default MTA threading model.

Designing Threaded Applications

A multithreaded program has two or more threads (flows of control) and can achieve significant performance gains with concurrency, with or without parallel thread execution. Concurrent thread execution means that two or more threads are executing at the same time. Parallelism occurs when two or more threads execute simultaneously across two or more processors.

In this section, we'll talk about real threading considerations and issues. Before you start developing applications, you should ask yourself these questions:

  1. Is it possible to subdivide the application to run on different threads?

  2. If it is possible to subdivide, how do I subdivide and what are the criteria for subdividing?

  3. What would be the relationship between the main thread and the worker threads? This defines how the tasks in the application will relate to each other.

You can determine the answer to the first question by inspecting the application. For example, does your application require heavy I/O operations, such as reading an XML file or querying a database, or perform a lot of CPU-intensive processing, such as encrypting and decrypting data, or hashing? If so, these operations could block your application's main thread.

If you've identified that parts of your application are potential candidates for separate threads, then you should ask yourself the following questions:

  1. Does each of the tasks identified use separate global resources?

    For example, if you've identified two potential threads for your application and they are both going to use the same global resource, such as a global variable or a DataSet object, then if both threads try to access the global resource at the same time, you could get inconsistent or corrupt data, as shown in the previous chapter. The only way to prevent this kind of problem is by using locks on the global resources, which could leave the other thread waiting. If both of the tasks are going to use the same global resource then it is not a good idea to break the task into two. For some resources, you could use the Monitor class to prevent the threads from locking up. Again, this was shown in Chapter 3.

  2. Over how long a period may the thread need to be blocked?

    It is not always possible to build applications that use independent global resources. For example, let's say two tasks in your application rely on a single global DataSet object. If the first task takes a long time to fill the DataSet object (let's say it fills about 50,000 rows from the database), then you would typically lock the DataSet object to prevent concurrency problems. Here a pseudo-code version of the first task:

     1. Open the Database connection 2. Lock the global DataSet object 3. Perform the query 4. Fill the DataSet with 50,000 rows from the database 5. Unlock the DataSet object 

    In this case, the second task needs to wait for a long time before it can access the DataSet object, which happens only when the first task finishes its execution and releases the lock. This is a potential problem and it will likely remove the concurrency of your application. There is a better way to address this problem:

     1. Open the Database connection 2. Perform the query 3. Fill the local DataSet with 50,000 rows from the database 4. Lock the global DataSet object 5. Set the local database to global dataset (DSGlobal = DSLocal) 6. Unlock the global DataSet object 

    In this way, we're not locking the global DataSet object until we need to update it and so we're reducing the time the lock on the global object is held.

  3. Does the execution of one task depend on the other task?

    For example, the tasks that you've identified could be querying the database and displaying the data in a DataGrid control. You can split the task into two by querying the database as the first task, and displaying the result in the DataGrid as the second task. The second task does not want to start until the first task has complete. Therefore, separating the querying and displaying the data in a DataGrid into two separate concurrently running tasks is not a viable option. One way around this is to have the first task raise an event when completed, and fire a new thread when this happens. Alternatively, you could use a timer that checks to see if is completed through a public field, and continues the execution of the thread when it has.

Threads and Relationships

The threads spun from a multithreaded application may or may not be related to each other. For example, in every application there will be a main thread that spins other threads and so the main thread becomes the controller of all other threads in the application. There are few common methods that can be used to define the relationship between the threads in a multithreaded application:

  • Main and Worker thread model

  • Peer thread model

  • Pipeline thread model

We will detail each of these models, including some code so that you can see how they might be implemented in your applications.

Main and Worker Thread Model

This is the commonest model and the one used throughout this book so far. It is illustrated in Figure 3:

click to expand
Figure 3

In the Main and Worker thread model, the main thread receives all input and passes it to other threads to perform particular tasks. The main thread may or may not wait for the worker threads to finish. In this model, the worker threads don't interact directly with the input sources as they read their input from the main thread. For example, we could have three buttons on a Windows Forms application that trigger three separate events:

  • Get data from a web service

  • Get data from a database

  • Do something else such as parsing an XML file

This is the simplest threading model. The main thread is contained within the Main() method, and the model is very common in client GUI applications.

Let's look at some code to demonstrate this. We'll use a form like the following:

When you click on a button, it will fire off a worker thread that will perform some calculations and return the results in the space below the buttons. We won't detail the UI code here; the full code can be downloaded from the http://www.Apress.com, but here are the relevant sections:

    public class MainWorker    {      public ArrayList CalculateFactors(int number)      {        if (number < 3)          return null;        else        {          ArrayList factors = new ArrayList();          factors.Add("1");          for (int current = 2; current <= number - 1; current++)          {            if ((int)(Math.Floor(number / current) * current) == number)              factors.Add(current.ToString());          }          factors.Add(number.ToString());          return factors;        }      }      public long CalculateFactorial(int number)      {        if (number < 0)          return -1;        if (number == 0)          return 1;        else        {          long returnValue = 1;          for (int current=1; current <= number; current++)            returnValue *= current;          return returnValue;        }      }   } 

The above methods are quite straightforward and are wrapped in a class for modularity reasons. The first returns an ArrayList containing all of the factors of the number passed to it, whereas the second simply returns a Long. Remember that factorials very quickly get very large. The factorial of 13 is 6,227,020,800. The factorial method doesn't tie up the processor for very long, but it can be used to illustrate this model.

    public frmCalculate()    {      //      // Required for Windows Form Designer support      //      InitializeComponent();      //Add any initialization after the InitializeComponent() call      threadMethods = new MainWorker();    } 

The constructor just contains an instantiation of a new MainWorker object that will be used in the methods. Below we show the methods used for the button click event handlers:

    private void cmdFactors Click(object sender, System.EventArgs e)    {      Thread calculateFactors = new          Thread(new ThreadStart(FactorsThread));      calculateFactors.Start();    }    void FactorsThread()    {      ArrayList val = threadMethods.CalculateFactors(200);      StringBuilder sb = new StringBuilder();      for (int count = 0; count <= val.Count - 1; count++)      {        sb.Append((string)val[count]);        if (count < val.Count - 1)          sb.Append(", ");      }      //Create and invoke the delegate with the new value      UpdateValue updVal = new UpdateValue(DisplayValue);      string[] Arugs = {sb.ToString()};      this.Invoke(updVal,Arugs);    } 

The cmdFactors_Click() method instantiates a new thread with the FactorsThread() method, which formats and acts upon the result contained in MainWorker.CalculateFactors().

start sidebar

This method will need to be wrapped because thread methods cannot have return values.

end sidebar

    private void cmdFactorial Click(object sender, System.EventArgs e)    {       Thread calculateFactorial =           new Thread(new ThreadStart(FactorialThread));       calculateFactorial.Start();    }    private void FactorialThread()    {      long val = threadMethods.CalculateFactorial(20);      //Create and invoke the delegate with the new value      UpdateValue updVal = new UpdateValue(DisplayValue);      string[] Arugs = {val.ToString()};      this.Invoke(updVal,Arugs);    } 

The FactorialThread() method is much simpler. Whenever the cmdFactorial button is clicked, the main thread fires off a new thread and updates the lblResult text label when the results have been achieved.

This was a straightforward example of main and worker threads in actions. Obviously, this example can easily be changed to deal with a connection to a database, or other more time-consuming activities.

However, you need to take care of several issues relating to threads when you use this mode. You can have threads spawning threads, threads accessing the same resource, and threads going into an infinite loop.

This is the simplest model, but also the one that requires most work from the developer.

In addition, the threads are completely independent of each other, each being controlled entirely by its parent - in this case the main thread.

Peer Thread Model

The next threading model we will describe is the Peer threading model. In this threading model, each thread will receive its own input from the appropriate sources and process that input accordingly. This model is illustrated in Figure 4.

click to expand
Figure 4

In the figure above, the UI thread will receive the input from the keyboard and mouse and it can work accordingly. Worker Thread A will listen to a particular socket and process input as it comes in from that socket, and in the same way Worker Thread B will wait for a system event and act accordingly. In this model, all the threads execute concurrently without blocking or waiting for other threads.

We can amend the previous example so that the CalculateFactors() method notices when the factorial thread finishes, and discovers the factors of this number. We will use the factorial of 8 in this example. In this example, however, we won't be using a socket, but just the setting of a variable. The principles will be the same for sockets; you would either continuously listen, or to save processor cycles, sleep intermittently.

So, let's change the WorkerThread class first:

    public class PeerThread    {      private int factorial;      public ArrayList CalculateFactors(int number)      {        if (number < 3)          return null;        else        {          ArrayList factors = new ArrayList();          factors.Add("1");          for (int current = 2; current <= number - 1; current++)        {          if ((int)(Math.Floor(number / current) * current) == number)            factors.Add(current.ToString());        }        factors.Add(number.ToString());        return factors;      }    }    public ArrayList CalculateFactors()    {      for(int count = 1; count<=30; count++)      {        Thread.Sleep(TimeSpan.FromSeconds(1));        if (factorial > 0)          break;        else if (count == 30 && factorial == 0)          return null;      }      ArrayList returnValue = CalculateFactors(factorial);      return returnValue;    }    public long CalculateFactorial(int number)    {      factorial = 0;      if (number < 0) return -1;      if (number == 0) return 1;      int returnValue = 1;      for (int current = 1; current <= number; current++)        returnValue *= current;      factorial = returnValue;      return returnValue;    } 

First, we'll explain the small changes. A private field has been created that will store the result of the factorial when it has been calculated. In addition, the class has been renamed to PeerThread. The CalculateFactors() method now has an overload, so that if it isn't passed an argument it performs the business end of this model.

All that happens is that the thread monitors the state of the factorial field, as if it were a socket, say. It checks to see if it is anything other than 0, and if so, it calls the CalculateFactors() method with the value of factorial as its argument and returns the ArrayList that it produces. We have also made a change in that we reset the factorial field at the start of the CalculateFactorial() method so there will always be some work to do. At the end of this method, we set factorial to equal the factorial.

Now, the frmCalculate class needs altering also. Observe the following changes:

    ...      private PeerThread threadMethods;      ...      public frmCalculate()      {        //        // Required for Windows Form Designer support        //        InitializeComponent();        //Add any initialization after the InitializeComponent() call        threadMethods = new PeerThread();      }      ...      private void NewFactorsThread()      {        ArrayList val = threadMethods.CalculateFactors();        calculateFactorial.Join();        StringBuilder sb = new StringBuilder();        for (int count = 0; count <= val.Count - 1; count++)        {          sb.Append((String)val[count]);          if (count < val.Count - 1)            sb.Append(", ");        }        //Create and invoke the delegate with the new value        UpdateValue updVal = new UpdateValue(DisplayValue);        string[] Arugs = {sb.ToString()};        this.Invoke(updVal,Arugs);      }      ...      private void cmdFactorial_Click(object sender, System.EventArgs e)      {        Thread CalculateFactors = new            Thread(new ThreadStart(NewFactorsThread));        Thread calculateFactorial = new            Thread(new ThreadStart(FactorialThread));        calculateFactors.Start();        calculateFactorial.Start();      } 

Apart from defining the new threadMethods field as a new PeerThread class, there are two important changes. We define a new method called NewFactorsThread(), which will call the CalculateFactors() method of the PeerThread class with no arguments. The rest of this method is the same.

In the cmdFactorial_Click() method, instead of just firing up the FactorialThread() method in a thread, we fire up the FactorsThread() as well, and execute them out of sequence so that calculateFactors may have to wait for calculateFactorial. You should be able to see how this can be tied into a network socket, and we will see an example of monitoring such a socket in Chapter 7.

The common problems that could occur with this kind of model are those of deadlocks and blocking. If you have a thread continually listening at a socket and getting its parameters from that socket, then that socket may need to be locked from other threads. This means, therefore, that this is a very good model to use for manager classes. We could use a thread to monitor a socket and then fire off new threads to process the contents. However, do not have more than one thread monitoring the same socket. In addition, if the thread is continually checking the socket or other resource, then it may consume more processor cycles than is necessary. As seen in the above example, you can use Thread.Sleep() to reduce the processor cycles expended.

As you will see, this example is also very similar to the Pipeline Thread Model in concept, but we are simulating a Peer Thread Model. If you click on the Calculate Factorials button, unless you have a very fast machine, you should have time to click on Calculate Factors, which will enable you to calculate and display the factors of 200, before it overwrites the textbox with the factors of 8!, or 40320.

Pipeline Thread Model

The Pipeline thread model is based on a series of tasks, each of which depends on the previous task. Have a look at Figure 5 which illustrates the situation:

click to expand
Figure 5

In the figure above, the main thread creates a series of threads, each of which will wait until the previous thread is finished executing. This kind of threading relationship is good if your task has certain stages and each of these stages is dependent on another. For example, your task could be processing input data and the process could have a few sub-tasks such as:

  • Filter all the non-valid characters, such as <, >, !, etc.

  • Check if the data is formatted correctly

  • Format all the numbers with currency sign and decimal points

  • Process the input

In this kind of situation, the next task can only be started if the previous task has finished. In the previous model, this is in effect what we were doing, as the factorial field was only set at the end of the thread. However, the proper way to implement the Pipeline thread model is to explicitly test that the thread has ended.

A few changes need to be made to the Peer thread example to make it part of the Pipeline model. First, we'll show the code for frmCalculate:

        ...        private PipelineThread threadMethods;        private Thread calculateFactorial;        ...        public frmCalculate()        {          ...          threadMethods = new PipelineThread();          ...        }        void NewFactorsThread()        {          ArrayList val = threadMethods.CalculateFactors();          calculateFactorial.Join();          ... 

As you can see, the changes are minimal here. The only interesting changes are the line that rescopes calculateFactorial, and the calculateFactorial.Join() line. This instructs the CalculateFactors thread to wait until calculateFactorial has completed before executing. The Join() call has to be within the thread whose execution you want to pause, and it is called on the thread it is waiting for. This inevitably means that the thread variable has to be rescoped to be at least class wide so that it can be accessed from other threads.

    public class PipelineThread      ...      public ArrayList CalculateFactors()      {        ArrayList returnValue = CalculateFactors(factorial);        return returnValue;      } 

Again, the changes here are small. The above method no longer has to check if factorial has been set, and is able to thus execute since it can assume the factorial has been calculated. This example could be useful when, for instance, you are waiting on a DataSet to fill, before performing some further calculations. This would enable the thread to fire as soon as the thread that fills the DataSet is complete. Of course, in a real application, error checking would have to be implemented as the thread could have completed but ended abruptly because of an error, or because of an Abort() instruction from another thread.

The traps you have to watch out for are the same traps that can occur with any thread. The first thread could be placed in an infinite loop, in which case, the second thread would never execute. By ensuring this can never happen in your first thread, you ensure that the second thread will execute and complete. In addition, you have to watch out for the thread ending unpredictably, due to an error or otherwise, as mentioned in the previous paragraph.

This concludes the discussion of the three models that can be applied to threading. By modeling your application to one of these, you should become familiar with the structure of the code you would need to use.



 < Day Day Up > 



C# Threading Handbook
C# Threading Handbook
ISBN: 1861008295
EAN: 2147483647
Year: 2003
Pages: 74

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