.NET, Programming

Multi-Threading Made Easy with .NET Framework 2.0

01.10.08 | Comment?

The .NET framework, and especially version 2.0, has made life a lot easier for programmers. In that past it wasn’t especially easy to create multi-threaded programs, but now it’s really pretty simple.

I have a program that downloads data from a large number of web sites (currently about 120), and in many cases the program had to sit doing nothing while waiting to receive its files. If I could have more than one operation executing at a time – asynchronously – the program would finish much more quickly. Here’s how I did it. (All examples in Visual Basic.)

First of all, create a function or subroutine that will go out and do the work. Test this by calling it in the normal, single-threaded way. Don’t try to run it on multiple threads until you’re sure it will run correctly in just one.

Here’s a simplified version of the function definition that I created:

Private Shared Function GetData(ByVal DataItem As WebData) As WebResult

This function accepts an object that contains data about what to download (the ‘DataItem’) and returns that data (of type ‘WebResult’).

Once I had this working, I next created my delegate, which looks very similar to the function definition:

Private Delegate Function GetDataAsync(ByVal DataItem As WebData) As WebResult

As you can see, this looks very much like the function definition above. The signature of this delegate – the number and type of arguments – must be identical to the original subroutine. But in fact it’s not a function definition at all. Instead it’s more like a class definition – in the next step, we’ll create a variable of type GetDataAsync.

Now you’re ready to call your function. As I just mentioned, first you create a variable of type GetDataAsync.

Dim GetDataCaller As New GetDataAsync(AddressOf GetData)

So what are we doing here? We’re creating a new variable using the delegate we defined above, and we’re passing it the address of the function that we want to call. You can pass any function here as long as the signatures of the function and the delegate are the same. Many event handlers work this way.

Now let’s kick off the flotilla of function calls:

For Each DataItem In DataItemList

     DataItem.ThreadHandle = GetDataCaller.BeginInvoke(DataItem, Nothing, Nothing)

So what’s going on here? We’re calling the BeginInvoke function of the class GetDataCaller (defined as a delegate by us, created as a class by the compiler). The first argument to BeginInvoke is a DataItem, the same as the underlying procedure GetData.

The other two parameters appear at the end of the argument list of every BeginInvoke call. The first is an AsyncCallback procedure. This argument represents the address of a function to be called when our GetData function is complete, to postprocess the results. As for the second parameter, according to Microsoft, “You can also pass an object containing information to be used by the callback method.” This object will normally be the delegate that was used to invoke the thread, so in this case we’d be passing GetDataCaller as the final parameter.

I’m not using a callback subroutine, so I am passing Nothing to these parameters, and since I haven’t fooled around with it I’m not going to embarrass myself by displaying my ignorance (at least, not any more than I already have).

The return value of the BeginInvoke function is an IAsyncResult object. This contains information about the thread invocation. For eample, you can poll the IAsyncResult’s IsCompleted member to see if the thread is done.

My code needs to stop at this point and wait until all of the worker threads are complete, so instead of using a callback, I’ll just loop through the list of objects and use EndInvoke.

For Each DataItem In DataItemList

     WebResultItem = GetDataCaller.EndInvoke(DataItem.ThreadHandle)
     ' Code to process WebResultItem goes here.

Now we call GetDataCaller’s EndInvoke method, passing the IAsyncResult that was returned by the BeginInvoke method, and capturing the output of the GetData in a variable of type WebResult. If a particular thread is not completed when you call EndInvoke on it, the program will wait until it has completed, at which point execution will continue – but we don’t care, because we have to wait until all these threads are complete before we can proceed anyway.

Another thing to notice is that I don’t have to worry about how many threads are available. Normally this is 25 per processor, and I’m firing off over a hundred at once, but .NET doesn’t return an error. It simply queues up the requests until a thread is available to process them.

The results? When I ran the program using only a single thread, it completed in around 2:15, or 135 seconds. I then ran the multi-threaded version, and it completed in only 45 seconds – just a third as long. This is pretty significant, since a job that would have taken half an hour can now be completed in ten minutes.

So to recap:

1) Create and debug the procedure you want to run on multiple threads. This is the most important part!

2) Define a delegate with the same signature as your procedure.

3) Create an object and use the name of the delegate you just created as its type.

4) Call BeginInvoke to start execution on a new worker thread.

5) Call EndInvoke to complete execution.

There’s more to this, including setting up callback procedures, waiting for a specified time (instead of forever), etc., but this should give you a start.

have your say

Add your comment below, or trackback from your own site. Subscribe to these comments.

Be nice. Keep it clean. Stay on topic. No spam.

You can use these tags:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>