Multi-threading in .NET 3.5
I realize that with AJAX, multi-threading is something that is seldom needed in the world of web programming. "Matthew, why are you wasting my time with this pointless post?" You may ask. Well, creating a web or windows service might require it. Let me give you an example.
You work a job as a programmer (shocker, I know). You get handed a task to create a service with a 30 minute heartbeat. Alright, no biggie, that should take a couple hours depending on the complexity needed. Wait a sec, Mr. Programmer, we need you to process 5000 records and send them via a third-party web service. *gulp* When you do the calculations for how much time it takes to process just one record and multiply it by 5000, you realize that there might be an issue because it will take around 45 minutes to process everything. *headslamdesk* The sky is falling, the world is ending and bring on the zombie apocalypse, right? WRONG!!! You know why? This is not a problem because there are people that beat their heads against the wall and have found a way to take care of this for you.
First off, instead of hand-feeding you something that could have disastrous results, let me tell you a bit about threading. Normal services or application rely on one thread, namely the Main thread. It goes about its business and does what you tell it to do. The memory usage is for it should be relatively low as long as you know what you are doing and when the thread ends, the service usually completes or the application is shut down. In the past, multi-threading was very attractive because the processing power was…well…not as fast or as inexpensive as it is now. You had a single core that needed to do things but you had to be very careful to free up the resources you used when multi-threading because there was no automatic cleanup of the memory being “reserved” for the threads.
As with all things, there is good news and bad news. The bad news…you still have to be careful about how you multi-thread. There are some tools that make things very easy to program *cough* semaphores *cough* but the problem with those is the inherent issue of trapping exceptions. Semaphores allow you to reserve a chunk of memory to process all of the threads, and be able to tell how many concurrent threads you want. (I usually go by the rule of four concurrent threads per processor because I work with production servers that have other applications and services running and I do not want to create a massive lag…I learned the issues of that while working at my previous job for a company that shall remain nameless) So you program in a semaphore and say that you want a maximum of four threads and a starting count of four threads locked. You assign the worker threads and tell the service to start…and you wait. The service completes and you tell it run again. It completes and you tell it run again. Do this a few times and you start to realize that the service is taking longer and longer with each iteration.
Why? Ahhhh…the issue is that, in .net 3.5, the memory being reserved for the process is not reusable and doesn’t free up at the completion of the service…oops. This is an issue.
In 3.5, .net still hasn’t fully worked out the bugs with multi-threading so we have to do it ourselves. I am a bit of a control freak when it comes to programming and I like to be able to insert a breakpoint and be able to see what has been completed, what needs to be completed and what the average process time for each record. Sound familiar? If you are a programmer, it should!
Today, we will be working with:
I will add each individual clock of code and explain it. At the very bottom, you will see a full version of my code. You will have to add your own processes to it and it will be generic because I cannot add too much of my actual code because of a NDA (Non-disclosure Agreement) I signed, but it should be enough to get you started.
using System.Collections;
namespace MyMultiThreadingService
static class MyMultiThreadingService
//I like to know the start and end time so I can get an average process time per record
static DataTable dtISend;
static DataTable dtISent;
static DataTable dtIResults;
private static ManualResetEvent resetEvents = new ManualResetEvent(false);
So, in the above block, I am setting up the objects I will need for my service. The only part that should look odd is the ManualResetEvent. Without getting too much into it, We need to have this inorder to use the threadpool. We have the option of an AutoResetEvent or ManualResetEvent. With the ManualResetEvent, I am setting the initial mode as false to show that it has not signaled that it is working. If I had set the initial value to true, I would have to change it later in order for the worker threads to know that it is not in the middle of a process.
int iFiles = dtISend.Rows.Count;
ThreadPool.SetMaxThreads(5, iFiles);
for (int i = 0; i <= dtISend.Rows.Count - 1; i++) ThreadPool.QueueUserWorkItem(new WaitCallback(SemFunction), (object)i);
The above is the main thread. The two voids that are used first (CreateDTSent and CreateDTResult are shown below. They create the two datatables that will house what files need to be sent and what the result was. There is a lag between when SemFunction is called for each worker thread and when it is completed so I wanted to be able to add a row to DTSent in order to keep the threads from duplicating their work, plus, since the path of the file is a primary key, I am trying to keep the errors from piling up. The WaitCallback object allows the item to be put on hold until thread is available to complete the next item. You will see that the parameters being used for the WaitCallback function are SemFunction and the index of the row being converted to an object. You will see SemFunction later on.
public static void CreateDTSent()
DataTable dt = new DataTable();
DataColumn[] key = new DataColumn[1];
DataColumn column = new DataColumn();
column.DataType = System.Type.GetType("System.String");
column.ColumnName = "file";
public static void CreateDTResult()
DataTable dt = new DataTable();
DataColumn[] key = new DataColumn[1];
DataColumn column = new DataColumn();
column.DataType = System.Type.GetType("System.String");
column.ColumnName = "file";
dt.Columns.Add("response");
Yup, the above code creates the datatables and sets the primary keys. Since I set up the datatables as static Datatables, they will maintain their values until the whole service is completed. Pretty cool, huh?
public static void WorkerFunction ()
DataTable dt = new DataTable();
dt.Columns.Add("xmlpath");
dt.Columns.Add("imgpath");
string[] paths = Directory.GetDirectories(@"C:\Documents and Settings\matthew_albright\My Documents\Visual Studio 2008\XML\Upload");
foreach (string str in paths)
string[] files = Directory.GetFiles(str);
DataRow dr = dt.NewRow();
string[] strSpl = str.Split(new string[] { "\\" }, StringSplitOptions.RemoveEmptyEntries);
dr[1] = strSpl[strSpl.GetUpperBound(0)];
dr[2] = files[1].ToString();
dr[3] = files[0].ToString();
for (int i = 0; i <= dtISend.Rows.Count - 1; i++)
q.Enqueue(dtISend.Rows[i][2].ToString());
In WorkerFunction, I am looking for all folders in a directory. Then I am looking in each of the folders for the first two files in them. I know that the first will be a .tiff file and that the second will be an .xml file. I getting their path and adding them into a datatable (dtISend). In my example, there are a total of 1876 folders, 1876 .xml files and 1876 .tiff files. At the bottom of the void, I am creating my queue. The Enqueue function adds items to a queue. Since I will only be sending the xml files, my queue will consist of 1876 paths, each to an .xml file.
private static void SemFunction(object o)
if (q.Contains((object)dtISend.Rows[i][2].ToString()))
string file = (string)q.Dequeue();
if (i == 0 || !vrbls.dtSent.Rows.Contains(file))
if (q.Count == 0) return;
Here is the SemFunction I promised that you would see again. I wanted to make sure that I showed you how to create the q and add items to it first because, here, we will be removing records from the queue. The main function here is Dequeue. This cool function calls the next item in the queue item collection and removes it.
This is the part that is a little irksome. It doesn’t remove it right away. Instead, it waits until the void that calls it is completed, THEN removes it. I added some checking to make sure that the path that the path from dtISend (or the path in the index of dtISend) is in the queue. If it is not in the queue, it has already been completed and the string exits the void and opens up for the next item to be completed. There is also the problem that the item might in the process of being worked on, so I added in the next part. Before the record gets sent to the third web service, I am creating a record for it in dtISent. That way, even if the item is in the process of being worked on, if the next thread tries to work on it, the record already exists in dtISent and the thread exits the void to work on the next item. The only problem with this is when the first item gets worked on. Since there is nothing in dtISent, this would normally cause an error…so I accounted for this by allowing the first record through without checking. When the queue is empty, all threads exit the void.
static void ISendFile(string file)
if (dtISend.Rows.Count > vrbls.dtSent.Rows.Count)
XmlDocument doc = new XmlDocument();
DataRow dr = vrbls.dtSent.NewRow();
if (vrbls.dtSent.Rows.Contains(file)) return;
vrbls.dtSent.Rows.Add(dr);
UpdateResults(SqlHelper.SendXML(doc),file);
if (q.Count == 0) resetEvents.Set(); finish = DateTime.Now;
DataRow drSent = vrbls.dtSent.Rows.Find(file);
static void UpdateResults(string response,string file)
DataRow dr = vrbls.dtResult.NewRow();
vrbls.dtResult.Rows.Add(dr);
string err = ex.Message.ToString();
In ISendFile, I create a record for the file being sent so that the next thread won’t inadvertently try to send the same file. It then gets sent and the response is captured in UpdateResults. If there is an error, I remove the record from the dtISent datatable and add the path back to the queue so it can be worked on again. Only do this after you know the code works, because the error might be something serious that needs to be captured.
When the queue count is zero then we call resetEvents.Set() which tells the threadpool that the tasks are completed and locks the ManualResetEvent. If you have any questions or need help, please feel free to contact me. Otherwise, happy coding and enjoy the world of multi-threading.