An Introduction to Threading in VB.NET

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

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

To understand threading in VB.NET, it helps to understand some of the foundation concepts. First up is that threading is something that happens because the operating system supports it. 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.

Definition of Thread

A thread is a single sequential flow of control.

Some qualifiers:

  • 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 has thread-specific data such as registers, a stack pointer, and a program counter.
  • 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).

This is assembly level stuff, but that's what you get into when you start thinking about threads.

Multithreading vs. Multiprocessing

Multithreading is not the same as multicore parallel processing, but multithreading and multiprocessing do work together. Most PCs 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 assigns a different process to different cores. Using multiple threads and multiple processors for even greater performance is called thread-level parallelism.

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 many problems that benefit from multiple threads. So, 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.

Practicing Thread Safety

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 disastrous 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. 

Basic Multithreading Operations

It's time to push this precautionary talk to the background and write some multithreading code. This article uses 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.

A Recursive Algorithm Example

Here's a multithreaded application involving calculating permutations of an array using a recursive algorithm. Not all of the code is shown here. The array of characters being permuted is simply "1," "2," "3," "4," and "5." Here's the pertinent part of the code.


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

However, 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" as mentioned earlier.

Race Condition Example

The first part of this article mentioned a race condition. Here's an example that shows it directly:


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