Threading with VB.NET - the proper way using the threadpool
With threading, I found it more efficient to build one threadmanager function that does the following: Starts up to 3 threadpools, accepts statuses from the threadpool at two times (1) when they start and (2) when they finish, returns a status of any or all threadpools. I do have each threadpool check a public boolean to identify if force exit is invoked - such as quitting a program. Hopefully this helps you I spent a few days here trying to understand threading, most help you look for on threading is not the proper coding technique. SEE BELOW
Threading Options
The .NET Framework offers two ways to implement multithreading. Regardless of which approach we use, we must specify the method or procedure that the thread will execute when it starts.
First, we can use the thread pool provided by the .NET Framework. The thread pool is a managed pool of threads that can be reused over the life of our application. Threads are created in the pool on an as-needed basis and idle threads in the pool are reused, thus keeping the number of threads created by our application to a minimum. This is important because threads are an expensive operating system resource.
Important The thread pool should be your first choice in most multithreading scenarios.
Many built-in .NET Framework features already use the thread pool. Any time we do an asynchronous read from a file, URL or TCP socket the thread pool is used on our behalf. Any time we implement a remoting listener, a Web site or a Web service the thread pool is used. Because the .NET Framework itself relies on the thread pool, we can have a high degree of confidence that it is an optimal choice for most multithreading requirements.
Second, we can create our own thread object. This can be a good approach if we have a single, long-running background task in our application. It is also useful if we need fine-grained control over the background thread. Examples of such control include setting the thread priority or suspending and resuming the thread's execution.
Using the Thread Pool
The .NET Framework provides a thread pool in the System.Threading namespace. This thread pool is self-managing. It will create threads on demand and, if possible, will reuse idle threads that already exist in the pool.
The thread pool won't create an unlimited number of threads. In fact, it will create at most 25 threads per CPU in the system. If we assign more work requests to the pool than it can handle with these threads, our work requests will be queued until a thread becomes available. This is typically a good feature, as it helps ensure that our application won't overload the operating system with too many threads.
There are four primary ways to use the thread pool: through BeginXYZ methods, via Delegates, manually via the ThreadPool.QueueUserWorkItem method, or by using a System. Timers.Timer control. Of the four, the easiest and most common is to use delegates.
Using BeginXYZ Methods
Many of the .NET Framework objects support both synchronous and asynchronous invocation. For instance, we can read from a TCP socket by using the Read method or the BeginRead method. The Read method is synchronous, so we are blocked until the data is read.
The BeginRead method is asynchronous, so we are not blocked. Instead, the read operation occurs on a background thread in the thread pool. We provide the address of a method that is called automatically when the read operation is complete. This callback method is invoked by the background thread, and so the result is that our code also ends up running on the background thread in the thread pool.
Behind the scenes, this behavior is all driven by delegates. Rather than exploring TCP sockets or some other specific subset of the .NET Framework class library, let's move on and discuss the underlying technology itself.
Using Delegates
A delegate is a strongly typed pointer to a function or method. Delegates are the underlying technology used to implement events within Visual Basic .NET (VB.NET), and they can be used directly to invoke a method given just a pointer to that method.
We've already seen how delegates allow us to transfer a method call from a background thread to the UI thread. We can also use them to launch a background task on a thread in the thread pool. To adapt our prime application to use delegates, we need to define a delegate for the FindPrimes method:
Private Delegate Sub Task()The only requirement here is that the delegate signature match the function signature. This means that we could simplify FindPrimes to accept the min and max values as parameters and provide those parameters via the delegate:
Private Delegate Sub Task(ByVal min As Integer, ByVal max As Integer)
Private Sub FindPrimes(ByVal min As Integer, ByVal max As Integer)
mResults.Clear()
For count As Integer = min To max Step 2
Dim isPrime As Boolean = True
For x As Integer = 1 To count / 2
For y As Integer = 1 To x
If x * y = count Then
' the number is not prime
isPrime = False
Exit For
End If
Next
' short-circuit the check
If Not isPrime Then Exit For
Next
If isPrime Then
mResults.Add(count)
End If
Next
End Sub
Running background tasks via delegates allows us to pass strongly typed parameters to the background task, thus clarifying and simplifying our code.
Also, notice that we've removed the code that invoked the DisplayResults method. We won't need that now either, because there's a simpler alternative at our disposal as we'll see in a moment.
Now that we have a delegate defined, we can change our click event handler code to use it to run FindPrimes on a background thread:
Private Sub Button1_Click(ByVal sender As System.Object)
ByVal e As System.EventArgs) Handles Button1.Click
' run the task
Dim worker As New Task(AddressOf FindPrimes)
worker.BeginInvoke(10001, 12000, AddressOf TaskComplete, Nothing)
End Sub
First we create an instance of the delegate, setting it up to point to the FindPrimes method. Then we call BeginInvoke on the delegate to invoke the method.
The BeginInvoke method is the key here. BeginInvoke is an example of the BeginXYZ methods we discussed earlier, and as you'll recall, they automatically run the method on a background thread in the thread pool. This is true for BeginInvoke as well, meaning that FindPrimes will be run in the background and the UI thread is not blocked, so it can continue to interact with the user.
Notice all the parameters we're passing to BeginInvoke. The first two correspond to the parameters we defined on our delegate�the min and max values that should be passed to FindPrimes.
The next parameter is the address of a method that will be automatically invoked when the background thread is complete. The final parameter (to which we've passed Nothing), is a mechanism by which we can pass a value from our UI thread to the method that is invoked when the background task is complete.
This means that we need to implement the TaskComplete method. This method is invoked when the background task is complete. It will run on the background thread, not on the UI thread, so we need to remember that this method can't interact with any Windows Forms objects. Instead it will contain the code to invoke the DisplayResults method on the UI thread via the form's Invoke method:
Private Sub TaskComplete(ByVal ar As IAsyncResult) Dim display As New Display(AddressOf DisplayResults) Me.Invoke(display) End Sub
Now when we run the application we'll have a responsive UI, with the FindPrimes method running in the background within the thread pool. Better still, we've simplified our code because we are able to pass parameter values to FindPrimes rather than resorting to the use of shared variables as in our previous examples.
Manually Queuing Work
The final option for using the thread pool is to manually queue items for the thread pool to process. This is done by calling ThreadPool.QueueUserWorkItem. This is a Shared method on the ThreadPool class that directly places a method into the thread pool to be executed on a background thread.
This technique doesn't allow us to pass arbitrary parameters to the worker method. Instead it requires that the worker method accept a single parameter of type object, through which we can pass an arbitrary value. We can use this to pass multiple values by declaring a class with all our parameter types. Add the following class inside the Form4 class:
Private Class params
Public min As Integer
Public max As Integer
Public Sub New(ByVal min As Integer, ByVal max As Integer)
Me.min = min
Me.max = max
End Sub
End Class
Then we can make FindPrimes accept this value as an Object:
Private Sub FindPrimes(ByVal state As Object)
Dim params As params = DirectCast(state, params)
mResults.Clear()
For count As Integer = params.min To params.max Step 2
Dim isPrime As Boolean = True
For x As Integer = 1 To count / 2
For y As Integer = 1 To x
If x * y = count Then
' the number is not prime
isPrime = False
Exit For
End If
Next
' short-circuit the check
If Not isPrime Then Exit For
Next
If isPrime Then
mResults.Add(count)
End If
Next
Dim display As New Display(AddressOf DisplayResults)
Me.Invoke(display)
End Sub
Also notice that we've reinstated the code to call DisplayResults on the UI thread when the task is complete. When we manually put a task on the thread pool, there is no automatic callback to a method when the task is complete, so we must do the callback here.
Now that FindPrimes accepts a parameter of type Object we can manually queue it to run in the thread pool within our click event handler:
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
' run the task
ThreadPool.QueueUserWorkItem(AddressOf FindPrimes, New params(10001, 12000))
End Sub
The QueueUserWorkItem method accepts the address of the worker method�in this case FindPrimes. This worker method must accept a single parameter of type Object or we'll get a compile error here.
The second parameter to QueueUserWorkItem is the object that is to be passed to the worker method when it is invoked on the background thread. In our case, we're passing a new instance of the params class we defined earlier. This allows us to pass our parameter values to FindPrimes.
When we run this code we'll again find that we have a responsive UI, with FindPrimes running on a background thread in the thread pool.
This page last updated May 6 2006