An Introduction to Threading in VB.NET

Making your program appear to do lots of things at the same time.

Hand and Cat's Cradle
Yagi Studio/Digital Vision/Getty Images

This article is the introduction to a series about threading in VB.NET. Later articles in this series cover:

-> The Form as an Application - The Message Pump and Apartment Threads
-> Joining Threads - Returning Control to a Main Thread
-> Threading - Locks: InterLock and SyncLock
-> Threading - Thread Communication and Events

To understand threading, we have to make sure some of the foundation concepts are clear first.

First up is that threading is something that happens because the operating system supports it. In particular, Microsoft Windows is a pre-emptive multitasking operating system. A part of Windows called the task scheduler parcels out processor time to all the running programs. These small chunks of processor time are called time slices. Programs aren't in charge of how much processor time they get, the task scheduler is. Because these time slices are so small, you get the illusion that the computer is doing several things at once.

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

A basic definition of a thread: A thread is a single sequential flow of control.

Some qualifiers:

-> A process is a single body of code that can have many threads but it has at least one and it has a single context (address space).
-> A thread is a "path of execution" through that body of code
-> Threads share memory so they have to cooperate to produce the correct result.


-> A thread will also have thread specific data such as registers, a stack pointer and a program counter.

This is assembly level stuff (see: The .NET Framework - Assemblies), but that's what you get into when you start thinking about threads.

One thing to be clear about is that this is not the same thing as multicore parallel processing, but multithreading and multiprocessing do work together.

Most PC's today have processors that have at least two cores and ordinary home machines sometimes have up to eight cores. Each "core" is a separate processor, capable of running programs by itself. You get a performance boost when the OS simply assigns a different process to different cores. Using multiple threads and multiple processors for even greater performance is called thread level parallelism (TLP).

A lot of what can be done depends on what the operating system and the processor hardware can do, not always what you can do in your program. And you shouldn't expect to be able to use multiple threads on everything. In fact, you might not find too many problems that will benefit from multiple threads a lot. So it's a good idea to approach this subject with an appropriately realistic attitude. Don't implement multithreading just because it's there. You can easily reduce your program's performance if it's not a good candidate for multithreading. Just as examples, video codecs may be the worst programs to multithread because the data is inherently serial. Server programs that handle web pages might be among the best because the different clients are inherently independent.

Framework 4.0 gave use a big boost because it introduced several new technologies that help you take advantage of multiple cores, such as:

-> The Framework 4.0 PLINQ (Parallel Language-Integrated Query).
-> The System.Threading.Tasks.Parallel class that can speed up loops like For and ForEach.
-> New types (classes) for parallel programming like concurrent collection classes, lightweight synchronization primitives, and types for lazy initialization

This is an introduction, so we won't get into detail about all of this, but you should know that Microsoft is focused on getting more performance out of todays multicore computers.

Multithreading is a tough problem! Multithreaded code often requires complex coordination of threads. Subtle and difficult-to-find bugs are common because different threads often have to share the same data so data can be changed by one thread when another isn't expecting it. The general term for this problem is "race condition".

In other words, the two threads can get into a "race" to update the same data and the result can be different depending on which thread "wins". As a trivial example, suppose you're coding a loop:


For I = 1 To 10
    DoSomethingWithI()
Next

If the loop counter "I" unexpectedly misses the number 7 and goes from 6 to 8 - but only some of the time - it would have disasterous effects on whatever the loop is doing. Preventing problems like this is called thread safety. If the program needs the result of one operation in a later operation, then it can be impossible to code parallel processes or threads to do it. An example of a race condition is included below, after we see what a basic thread is.

Basic Multithreading Operations

Let's push all this precautionary talk to the background and actually write some multithreading code! We'll use a Console Application for simplicity right now. If you want to follow along, start Visual Studio with a new Console Application project.

The primary namespace used by multithreading is the System.Threading namespace and the Thread class will create, start, and stop new threads. In the example below, notice that TestMultiThreading is a delegate. That is, you have to use the name of a method that the Thread method can call.


Imports System.Threading
Module Module1
    Sub Main()
        Dim theThread _
            As New Threading.Thread(
                AddressOf TestMultiThreading)
        theThread.Start(5)
    End Sub
    Public Sub TestMultiThreading(ByVal X As Long)
        For loopCounter As Integer = 1 To 10
            X = X * 5 + 2
            Console.WriteLine(X)
        Next
        Console.ReadLine()
    End Sub
End Module

In this app, we could have executed the second Sub by simply calling it:


TestMultiThreading(5)

This would have executed the entire application in serial fashion. The first code example above, however, kicks off the TestMultiThreading subroutine and then continues.

To demonstrate this conclusively, I've coded a different multithreaded application using an article I wrote some years ago involving calculating permutations of an array using a recursive algorithm.

I picked this because I wanted something that would take some significant processor time to execute. I won't show all of the code in this article. If you want to read the original, you can find it at: Permutations, Recursion, and Traversing a Binary Tree. The array of characters being permuted is simply "1", "2", "3", "4", "5". Here's the code (except for the details you can find in the original article).


Sub Main()
    Dim theThread _
        As New Threading.Thread(
            AddressOf Permute)
    'theThread.Start(5)
    'Permute(5)
    Console.WriteLine("Finished Main")
    Console.ReadLine()
End Sub

Sub Permute(ByVal K As Long)
	...
	Permutate(K, 1)
	...
End Sub
Private Sub Permutate( ...
	...
	Console.WriteLine(
		pno & " = " & pString)
	...
End Sub

Notice that there are two ways to call the Permute sub (both commented out in the code above). One kicks off a thread and the other calls it directly. If you call it directly, you get:


1 = 12345
2 = 12354
... etc
119 = 54312
120 = 54321
Finished Main

But if you kick off a thread and Start the Permute sub instead, you get:


1 = 12345
Finished Main
2 = 12354
... etc
119 = 54312
120 = 54321

This clearly shows that at least one permutation is generated, then the Main sub moves ahead and finishes, displaying "Finished Main", while the rest of the permutations are being generated. Since the display comes from a second sub called by the Permute sub, you know that is part of the new thread as well. This illustrates the concept that a thread is "a path of execution" mentioned earlier.

You can do some interesting experiments with this simple example. Here's one. I wondered what would happen if I executed the Permute sub both directly and as a separate thread. To be able to determine which is which, I modified the code so I could pass different Char arrays to the Permute sub.

(Note: the leading "x" is there because the permutation algorithm uses a 1 based array rather than .NET's 0 based array. This was just a quick way to avoid having to recode it for .NET.)


Dim inCharsNum() As Char = {"x", "1", "2", "3", "4", "5"}
Dim inCharsChar() As Char = {"x", "A", "B", "C", "D", "E"}
Dim theThread _
    As New Threading.Thread(
        AddressOf Permute)
theThread.Start(inCharsChar) ' <- thread started with chars
Permute(inCharsNum)          ' <- direct call with numbers
Debug.Print("Finished Main")

The result was mildly surprising:


1 = ABCDE
1 = 12345
2 = ABCED
Finished Main
3 = ABDAB
4 = ABDBA
... etc

The secondary thread "took over" the direct call, which never finished. This happens because both call another sub which depends on data that both modify. This is exactly the kind of "thread safety" problem that you run into when you start using multithreaded code.

This is a good opportunity to provide an example of another thing missing from the Microsoft documentation: How to pass multiple values to the Thread.Start() method.

If you have been following along carefully, you might have noticed that I didn't pass the array length parameter K in the example above. Instead, I recoded the entire program to accept the array of characters ("A" through "D" or "1" through "5") instead and hardcoded the array length K as 5. I did this for two reasons. First, to make the example easier to read and second, because Thread.Start will only accept a single parameter. But the parameter is of type Object, so all we have to do to pass both parameters (K and the Char arrray) is create a structure.


Structure PermuteStruct
    Dim inCharsArray() As Char
    Dim KValue As Integer
End Structure

Then initialize the structure and pass that:


Dim thePermuteObject As New PermuteStruct
thePermuteObject.inCharsArray = inCharsNum
thePermuteObject.KValue = 5
Dim theThread _
    As New Threading.Thread(
        AddressOf Permute)
theThread.Start(thePermuteObject)

Of course, I still had to reprogram the method to accept this new structure, but the point is that two values were now being passed instead of one.

If you have this much working, then the rest is just careful programming. The Threading namespace has lots of methods and properties that you can use to customize your application. For example, you can set the priority of a thread so it doesn't chew up your processor. I set the priority of the secondary thread high and the Main thread low and got this result:


Dim theThread _
    As New Threading.Thread(
        AddressOf Permute)
theThread.Priority = ThreadPriority.Highest
Thread.CurrentThread.Priority = ThreadPriority.Lowest
theThread.Start(thePermuteObject)

1 = 12345
2 = 12354
3 = 12435
4 = 12453
5 = 12534
6 = 12543
Finished Main
7 = 13245
8 = 13254
... etc

Full disclosure: I also had to start about a dozen YouTube videos to take up the slack on my PC before this effect could really be seen. PC's are fast these days!

Race Conditions

In the first part of this article, I promised to show an example of a race condition. The example just shown might be considered one example already, but here's a different one that shows it more directly. First, the code.


Module Module1
    Dim I As Integer = 0
    Public Sub Main()
        Dim theFirstThread _
            As New Threading.Thread(
                AddressOf firstNewThread)
        theFirstThread.Start()
        Dim theSecondThread _
            As New Threading.Thread(
                AddressOf secondNewThread)
        theSecondThread.Start()
        Dim theLoopingThread _
            As New Threading.Thread(
                AddressOf LoopingThread)
        theLoopingThread.Start()
    End Sub
    Sub firstNewThread()
        Debug.Print(
            "firstNewThread just started!")
        I = I + 2
    End Sub
    Sub secondNewThread()
        Debug.Print(
            "secondNewThread just started!")
        I = I + 3
    End Sub
    Sub LoopingThread()
        Debug.Print(
            "LoopingThread started!")
        For I = 1 To 10
            Debug.Print(
                "Current Value of I: " &
                I.ToString)
        Next
    End Sub
End Module

The Immediate window showed this result. (In one trial. ... Other trials were different. That's the essence of a race condition.)


LoopingThread started!
Current Value of I: 1
secondNewThread just started!
Current Value of I: 2
firstNewThread just started!
Current Value of I: 6
Current Value of I: 9
Current Value of I: 10

Notice that we have three threads, all modifying a global variable I. What if your program depended on the value of I? This is such a common problem that .NET has a built-in solution. The article Threading - Locks: InterLock and SyncLock shows you how that's done.