Monday, July 14, 2008

Visual Studio Threading – Part II

This is a part of a series of articles on my experiences with Visual Studio Threading and Advantage Events.

In my second posting about my threading experience with Visual Studio I did some testing using the ThreadPool object. Once again I do not claim to be an expert I am just sharing my experiences with threading and how it can be used to work with Advantage.

With a brand new roll of duct tape handy I began my searching for some Thread Pool examples. I started with MSDN and found a good example here. Another good discussion of the ThreadPool can be found on C# Corner. Finally I continued to use Threading in C# by Joseph Albahari as a reference.

A ThreadPool class creates a collection of threads which can be used to run various operations. The ThreadPool makes efficient use of many wait handles on a limited number of threads (25 by default) allowing rapid series of calls to be executed efficiently. However, it is pretty difficult to execute these threads in an asynchronous manner.

I found the ThreadPool to be most efficient if you need to perform several operations at once and then move on to other tasks. The examples showed using the pool to queue up several items to be processed and then waiting until all the processing was completed. This can be very useful for many Advantage applications but is not a good fit when waiting on events.

I used the Microsoft ThreadPool example as a template and created a simple demonstration of opening several tables and putting them into a DataSet object. In order to do this I first had to make a class which contains the logic for retrieving the data from Advantage. The class also includes a wrapper which can be used when calling it from a ThreadPool.

   1: class AdsQuery
   2: {
   3:     // Property variables
   4:     private DataSet _ds;
   5:     private DataTable _dt;            // Query Results
   6:     private string _cmdText;        // Query text
   7:     private string _connString;     // Connection String
   8:     private ManualResetEvent _doneEvent;
   9:  
  10:     // Public properties
  11:     public string CommandText 
  12:     { 
  13:         get { return _cmdText; }
  14:         set { _cmdText = value; }
  15:     }
  16:  
  17:     public string ConnectionString
  18:     { 
  19:         get { return _connString; }
  20:         set { _connString = value; }
  21:     }
  22:  
  23:     public DataTable Results
  24:     {
  25:         get { return _dt; }
  26:     }
  27:  
  28:     
  29:     // Constructor
  30:     public AdsQuery(string Command, string Connection, ManualResetEvent doneEvent)
  31:     {
  32:         _cmdText = Command;
  33:         _connString = Connection;
  34:         _doneEvent = doneEvent;
  35:         _ds = new DataSet();
  36:         _dt = new DataTable();
  37:     }
  38:  
  39:     // Wrapper method for use with a thread pool
  40:     public void ThreadPoolCallback(Object threadContext)
  41:     {
  42:         int threadIndex = (int)threadContext;
  43:         Console.WriteLine("thread {0} started...", threadIndex);
  44:         if (RunQuery())
  45:             Console.WriteLine("thread {0} resultset retrieved ...", threadIndex);
  46:         else
  47:             Console.WriteLine("thread {0} query error ...", threadIndex);
  48:         _doneEvent.Set();
  49:     }
  50:  
  51:     // Opens a connection and runs the specified query
  52:     public bool RunQuery()
  53:     {
  54:         AdsConnection cn = new AdsConnection(_connString);
  55:         AdsCommand cmd = cn.CreateCommand();
  56:  
  57:         // Configure the command
  58:         cmd.CommandTimeout = 0;
  59:         cmd.CommandText = _cmdText;
  60:  
  61:         try
  62:         {
  63:             cn.Open();
  64:             AdsDataAdapter da = new AdsDataAdapter(cmd);
  65:             da.Fill(_ds);
  66:             _dt = _ds.Tables[0];
  67:         }
  68:         catch (AdsException aex)
  69:         {
  70:             return false;
  71:         }
  72:         finally
  73:         {
  74:             cn.Close();
  75:         }
  76:  
  77:         return true;
  78:     }
  79: }

In this simple test the wrapper function writes out the thread status to the console. I did a very simple error trap for this example but you would want to make this more robust for actual use. The second step was to create my pool and send it some work. For simplicity I just sent it the same query 10 times.

   1: static void Main(string[] args)
   2: {
   3:     const int Queries = 10;
   4:  
   5:     // One event is used for each query to be run
   6:     ManualResetEvent[] doneEvents = new ManualResetEvent[Queries];
   7:     AdsQuery[] qryArray = new AdsQuery[Queries];
   8:     
   9:     // Configure and launch threads using ThreadPool:
  10:     Console.WriteLine("launching {0} tasks...", Queries);
  11:     for (int i = 0; i < Queries; i++)
  12:     {
  13:         doneEvents[i] = new ManualResetEvent(false);
  14:         AdsQuery q = new AdsQuery("SELECT * FROM Customer", "Data Source=C:\\Data;ServerType=Remote;", doneEvents[i]);
  15:         qryArray[i] = q;
  16:         ThreadPool.QueueUserWorkItem(q.ThreadPoolCallback, i);
  17:     }
  18:  
  19:     // Wait for all threads in pool to finish
  20:     WaitHandle.WaitAll(doneEvents);
  21:     Console.WriteLine("All queries have been run.");
  22:  
  23:     // View the first field from the first record of each data table
  24:     for (int i = 0; i < Queries; i++)
  25:     {
  26:         Console.WriteLine("Table {0} Record count {1}", i, qryArray[i].Results.Rows.Count);
  27:     }
  28:  
  29:     // Wait so we can see the results
  30:     Console.Write("Press [Enter] to exit.");
  31:     Console.ReadLine();
  32: }

The QueueUserWorkItem requests a thread from the ThreadPool which executes the specified method, in this case our wrapper function in the AdsQuery class. Each instance of AdsQuery has its own event which is signaled, using the Set() method, when the query has finished. The main thread waits for all the operations to finish using the WaitAll method. After all the threads are finished then the recordcount for all the retrieved tables is displayed. The results of the test are below.

Thread Pool Results

One last item to remember threads within the ThreadPool are background threads and they do not keep the application alive. Therefore if you end the application while a thread within the ThreadPool is active it can cause a race condition. You must ensure that none of the threads in the pool are currently busy before closing the application.

Once again while there are many practical uses for a ThreadPool it doesn’t seem to be the right fit for use with Advantage Events. In the next part I will discuss using a BackgroundWorker.

No comments: