1. Computing

Threading - Using the ThreadPool Class

A Microsoft Example Explained and Expanded

By

Updated January 16, 2012

This article, one in a series on Visual Basic .NET and threading, introduces the techniques used in this multiprocessing technique. The first article contains a list of all of the articles in this series.

-> Threading: Making your program appear to do lots of things at the same time.

Suppose you want to kick of an some worker processes to do something. For example, write some files with different information provided by a calling program. In a real application, this might be sending these files over a network to a collection of remote machines. In this example, I'm just going to create the files in a root directory.

It could be very useful to do this as a multithreaded application because it's impossible to predict just how long it will take to send each file to the target machine. So, what you want to do is kick off the necessary independent threads that will each do their individual jobs. You can finish the main application only when all of them have completed or stop it immediately. Previous examples have waited on several threads to complete using the WaitOne method. For this type of application, WaitAny is what you want to use.

Microsoft has a number of great multithreading examples. Rather than using one that is completely different, I decided to expand and explain one from the MSDN documentation in this article. The example I started with is a Console application used to illustrate the WaitAll method and writes five files five threads started by a loop. I decided to see if I could kick off these threads from a Windows form application that provides the information used to write the files.

The initial form used to start the application looks like this:

--------
Click Here to display the illustration
--------

I used a ListView control to provide the data because I want to have several independent fields in each row of the control. In this case, the data was created in the ListView using the Visual Studio wizard built into the properties of the control. You can learn more about the ListView control in the article ListBox, Dictionary, and ListView Example.

One problem with using multithreading and a Windows form application is that Windows form apps are STA (Single Threaded Apartment model) and don't work well with the MTA ("multi" instead of "single") model necessary to write multiple files asynchonously. (You can use delegates and Invoke methods to update a form from another thread in Windows form applications. This example doesn't do that. It uses a new process controlled by a Windows shell.) To get the convenience of a Windows form and still use multithreading, this example writes everything to a file and then kicks off a MTAThread console application using Process.Start. This makes things work independently, but it also introduces other problems. For one thing, Visual Studio won't debug the console app and you can have difficulty even seeing the errors. You can attach a debugger to the console app, but I found it easier to simply copy the app to a new standalone project and debug it there.

Rather than creating new threads like the previous examples, this example uses threads from the Windows thread pool provided by the .NET Framework through the ThreadPool class. Each process in .NET provides a pool of threads that your code can use. This can make your application perform better because when a new thread is started, microseconds are spent organizing resources that the thread needs along with about a meg of memory. A thread pool shares and recycles threads instead and the ThreadPool class optomizes the use of multiple processors on your computer by only allocating threads when processors are available. The key method used is the ThreadPool.QueueUserWorkItem method which assigns a thread from the pool.

If you have a resource intensive application, you can monitor the threads being used using other methods in the ThreadPool namespace. This illustration shows the GetMaxThreads method:

--------
Click Here to display the illustration
--------

The Windows app is very simple and consists only of a loop to read the rows from the ListView and write the file and call to Process.Start.


Dim listViewItemString As String
Dim fileWriter As System.IO.StreamWriter
fileWriter =
    My.Computer.FileSystem.OpenTextFileWriter(
        "..\..\..\fileparms.dat", False)
fileWriter.WriteLine(lvwFileInformation.Items.Count)
For Each listDataItem As  _
    ListViewItem In lvwFileInformation.Items
    listViewItemString =
        listDataItem.Text.PadRight(8) &
        listDataItem.SubItems(1).Text.PadLeft(2) &
        listDataItem.SubItems(2).Text()
    fileWriter.WriteLine(listViewItemString)
Next
fileWriter.Close()
Dim myProcess As New Process()
myProcess.StartInfo.FileName = "WriteTheFiles.exe"
myProcess.StartInfo.CreateNoWindow = True
myProcess.Start()
' The End statement stops the Windows form
' application immediately
End

For convenience, both projects (the Windows form and the console applications) are part of the same solution. This allows both executable files to be written to the same directory in the solution and that's why no directory is specified in the code. The parmfile.dat file, however, is written to the common root for both using the "..\..\..\" relative path. This is used in the console application as well.

Here's the flow of processing used by the console application that writes the files:

--------
Click Here to display the illustration
--------

The data from parmfile.dat is accessed with this code:


fileReader.TextFieldType =
    FileIO.FieldType.FixedWidth
fileReader.FieldWidths = ({-1})
currentRow = fileReader.ReadFields()
fileCount = CInt(currentRow(0))
Dim manualEvents(
    fileCount - 1) As ManualResetEvent
fileReader.FieldWidths = ({8, 2, -1})
While Not fileReader.EndOfData
    currentRow = fileReader.ReadFields()

A New instance of a State class (described later) is created for each thread, a thread is retrieved from the ThreadPool object, and the thread is queued up for processing by the same WriteToFile subroutine with the instance of the State class passed as a parameter. Note that the End While for the read loop While above is here.


    manualEvents(i) =
        New ManualResetEvent(False)
    stateData =
        New State(
            fileName,
            numberToWrite,
            byteArray,
            manualEvents(i))
    ThreadPool.QueueUserWorkItem(AddressOf _
        Writer.WriteToFile, stateData)
    i += 1
End While

The manualEvents array is an important key in making this work. Possibly the most important statement in the project uses this array to wait for the completion of all of the threads writing files. WaitAll and WaitAny both require an array like this to be passed containing the handles of any threads that must complete.


WaitHandle.WaitAll(manualEvents)

A class named State is created to hold all information for writing each file, plus a ManualResetEvent that will be Set when the thread writing the file completes.


Public Class State
    Public fileName As String
    Public numberToWrite As Integer
    Public byteArray As Byte()
    Public manualEvent As ManualResetEvent
    Sub New(
           fileName As String,
           numberToWrite As Integer,
           byteArray() As Byte,
           manualEvent As ManualResetEvent
           )
        Me.fileName = fileName
        Me.numberToWrite = numberToWrite
        Me.byteArray = byteArray
        Me.manualEvent = manualEvent
    End Sub
End Class

The Writer.WriteToFile sub is executed in each thread started. The Set method for the manualEvent passed as part of the state object is executed in the Finally clause of the Try block to ensure that all threads are signaled.


Public Class Writer
    Shared Sub WriteToFile(state As Object)
        Dim stateInfo As State = CType(state, State)
        Dim fileWriter As FileStream = Nothing
        Try
            fileWriter = New FileStream("..\..\..\" &
                stateInfo.fileName, FileMode.Create)
            fileWriter.Write(stateInfo.byteArray,
                0, stateInfo.byteArray.Length)
        Finally
            If Not fileWriter Is Nothing Then
                fileWriter.Close()
            End If
            stateInfo.manualEvent.Set()
        End Try
    End Sub
End Class

The end result is four files in the common root directory.

--------
Click Here to display the illustration
--------

In the first code snippet above, I note that an End statement stops the Windows form app right after the console app is started. You can test to see that this is, in fact, what happens by adding a Sleep statement as the first executable statement in the Main sub.


' Sleep for 30 seconds
Threading.Thread.Sleep(30000)

Monitor the processes in Windows Task Manager while running the solution and you will see that the Windows form project process is gone well before the console app process that writes the files completes.

  1. About.com
  2. Computing
  3. Visual Basic
  4. Threading and Multiprocessing
  5. Threading - Using the ThreadPool Class

©2014 About.com. All rights reserved.