For any computer programmer, threads, like pointers, are one of those
hurdles that seem to take forever to
climb over. No matter how many times you
read that passage in the book or the notes from class, it seems that the day of understanding will never come...until one day, the
key log breaks in two and everything becomes clear. Here's an explanation of my own on the concept written in
VB.NET-ese that, if you're having trouble like I did, maybe will get the logs
flowing again for you.
A Brief Recap
All those programs running on your computer right now (your
browser, your media player, your virus scanner,
all that spyware) are threads. You see, your computer may
appear to be running all of those programs simultaneously, but really, the thing can only run one program at a time. What the programs do is
take turns running. And they do this really really
really quickly, which makes it
seem like everything's running all at once.
The term "thread" is
just a word for a block of computer code that needs to be run
independently of other blocks of computer code. Each program doing its thing on your
information machine is what's called a "
process". And within each of those processes, there is at least one, but usually more than one, thread running. For example, when visiting a page in, say,
MySpace, you might be dazzled by
twenty different animating images, startled by some loud noise that
sounds like music, and frenzied into scrolling down the page all at once. A thread has to be created for each of those actions inside your browser in order for these things to happen at once. What I'm going to explain here is how to write code for a VB.NET program (a process) which creates its own threads.
First off, you're going to want to always add the System.Threading import to whatever class you'll be creating threads in. Without that at the top of your code,
nothing will work. That being said, there are two main ways that you can
actually start a thread in VB.NET.
-
Use the Thread class
Dim someThread As New Thread(AddressOf SomeMethod)
someThread.Start()
This spawns a totally new thread, executing the code inside the SomeMethod method (which you define yourself). Don't forget that second line of code or else the thread will never start running and you'll be spending hours trying to figure out why (first-hand experience here). Also note that you could also do something like this:
Dim someClass As New SomeClass
Dim someThread As New Thread(AddressOf someClass.SomeMethod)
someThread.Start()
This will spawn a thread which runs inside the individual instance of that class.
-
All yarn outta the pool
Dim someObject as New Object
ThreadPool.QueueUserWorkItem(AddressOf SomeMethod, someObject)
The ThreadPool class is basically a list that manages up to twenty-five different threads. All threads the pool have a priority of "normal" (I'll explain more about that soon), which cannot be changed. From what I've read, it's suggested that you use this technique instead of the latter unless you're desperate for more control over your code.
-
Note: Thread is NonInheritable
For any Java programmers out there, VB.NET does not allow you to create a class which inherits Thread. Being from a Java background myself, this absolutely bedazzled me.
"It's either the car or ME!"
Each thread running on your
box has a priority. Those with a high priority get to take turns more often than those with a
low priority. VB.NET lets you control the priority of your threads. The exception to this is the threads that are added to the ThreadPool; their priorities are
set in stone.
Dim someThread as New Thread(AddressOf SomeMethod)
someThread.Priority = ThreadPriority.
Highest
Use the ThreadPriority
enumeration to control how often the thread is allowed to run. The default setting for this property is ThreadPriority.Normal and it's suggested that, unless you know what you're doing, you leave it that way.
Dim someThread as New Thread(AddressOf SomeMethod)
someThread.IsBackground = True
In addition, you can define your thread as a "
background" thread. Threads by default are not background threads so the programmer is
responsible for stopping any threads he created when the program exits (or else the program
will never really exit). If this property is set to True, then the thread will be automatically killed when the program exits. This can be a
good thing or a
bad thing, depending on what your thread is doing, so
keep an eye out.
Left, one two three, right one two three
Synchronization is by far the more difficult aspect of threading to
wrap one's mind around. If you have two threads, each executing the same code inside the same exact method, and that method's code modifies data which is shared by those threads, then you run into a
big problem.
You have no control over when the thread A gives up its turn to thread B. Thread A could be in the middle of a
calculation and thread B could start running, modifying one of the shared variables, thereby ruining the result of thread A's calculation. The programmer must tell the computer where these critical sections of code are--sections where the thread must
never relinquish its control to another thread.
It accomplishes this by using what's called a "
lock." If a thread reaches critical code which is protected by a lock, it
waits until the lock is available before taking possession of it and running the critical code. When it finishes executing the code, the thread then releases the lock so that another thread that is waiting to run the code now can.
Now,
as you might expect, being the
creatures of free will that we all are, we have many choices at our disposal with which to implement thread synchronization (in VB.NET anyway).
The first set of choices are the
simplest:
-
Dim lockObject As New Object
Sub SomeMethod()
SyncLock lockObject
'critical section goes here
End SyncLock
End Sub
This is the simplest way of accomplishing synchronization. The lockObject must be (1) not equal to Nothing and (2) a reference object (i.e. not a primitive like Integer or Double).
-
Imports System.Runtime.CompilerServices
Class SomeClass
<MethodImpl(MethodImplOptions.Synchronized)> _
Sub SomeMethod()
'critical code
End Sub
End Class
The mess of code you see above makes the entire method synchronized. It's just like encasing all the method's code in a SyncLock block, but without having to declare a special object to act as the lock. If you have multiple methods in the same class using this, they each share the same lock object behind the scenes so only one of the synchronized methods can be accessed at a time.
-
Imports System.Runtime.Remoting.Contexts
<Synchronization()> _
Class SomeClass
Inherits ContextBoundObject
'everything is critical!
End Class
This makes everything inside your entire class critical, which is nice if you want an easy way to avoid synchronization bugs. However, from what I've read, that's usually overkill. Plus, you're forced to inherit some stupid object.
This next set of choices are class-based and give you more control. But with that control comes
responsibility. You must
always remember to
manually release the lock when you are finished...it won't be done
automatically for you like the above implementations. Because of this, it's
often good to enclose the lock/unlock method calls in a Try/
Finally block to ensure that the lock will always be released if the code decides to throw an
exception and exit the method
prematurely.
Note: Before many of the examples, I'm going to give a few
equivalences (which are not code themselves) which compare the functionality of the class under discussion to
previously discussed synchronization techniques.
-
Dim lockObject As New Object
Monitor.Enter(lockObject) === SyncLock lockObject
Monitor.Exit(lockObject) === End SyncLock
Dim lockObject As New Object
Sub SomeMethod()
If Monitor.TryEnter(lockObject) Then
Try
'you've obtained the lock and have entered the critical section
Finally
Monitor.Exit(lockObject)
End Try
Else
'No lock for you!
End If
End Sub
The Monitor object contains all the functionality of the SyncLock block plus an additional feature. The TryEnter() shared method returns True and acquires the lock if the lock can be acquired or False if the lock cannot be acquired (it will not wait to acquire the lock like the Enter() method does).
-
Dim lockCounter As Integer
Sub SomeMethod()
Try
If Interlocked.Increment(lockCounter) <= 4 Then
'critical section
Else
'there are more than four threads in the critical section...no lock for you!
End If
Finally
Interlocked.Decrement(lockCounter)
End Try
End Sub
This class acts like Monitor.TryEnter() in that it will try to obtain the lock, but won't wait for it. What is unique here is the fact that you can control just how many threads are allowed to access the critical section. The example above allows a max of four threads inside at once. Since you're only incrementing and decrementing an integer, I don't know why you need an entire class to do this, but I could be missing something.
-
Dim someMutex As New Mutex
Dim lockObject As New Object
someMutex.WaitOne() === Synclock lockObject
someMutex.ReleaseMutex() === End Synclock
someMutex.WaitOne(100, False) === Monitor.TryEnter(lockObject, 100)
In addition to the equivalences you see above, the Mutex class has two handy shared methods:
Dim someMutexes() As Mutex = {New Mutex, New Mutex, New Mutex}
Sub SomeMethod()
Dim someInteger As Integer
someInteger = Mutex.WaitAny(someMutexes)
Try
'critical section
Finally
someMutexes(someInteger).ReleaseMutex()
End Try
End Sub
This will cause the thread to wait until one of the Mutex objects in the array is released, at which point it returns the array index of the Mutex, acquires it, and moves on.
Dim someMutexes() As Mutex = {New Mutex, New Mutex, New Mutex}
Sub SomeMethod()
Mutex.WaitAll(someMutexes)
'all Mutex objects are now released
End Sub
This waits until all Mutex objects in the array are released. I guess this could be good for ensuring that all your threads complete before your program terminates.
Dim someMutex As New Mutex("False", "some name")
Another feature of this class lies in the programmer's ability to assign a name to each Mutex he creates. The spiffy thing about this is that if a Mutex exists anywhere in the system (not just in your program) with that name, it becomes associated with that Mutex.
And I'm spent
And so ends my tutorial on threading in VB.NET. For more specific knowledge on how the VB.NET code works, take a look at the MSDN
documentation. Again, threads take some time to get used to, but once understood, I
guarantee the ladies will be
all over you.
References:
- Balena, Francesco. Programming Microsoft Visual Basic .NET (Version 2003). Microsoft Press. 2004.
- MSDN Documentation. <http://msdn.microsoft.com/vbasic/>
Swap asks, "Do you know how this works within the Mono framework?"
I have absolutely zero experience with Mono, but from how fabulous I've heard it's supposed to be, I would think that the things I discussed in my node would apply to it as well. The only exception I can think of is concerning mutexes. I'm taking a wild guess here, but they might be a Windows-only thing, and since they seem to be tied really close to the O/S, Mono (for *nix) might handle them differently. Perhaps someone could correct me?
Thanks to rootbeer277 and call for pointing out some typos.