This is the second of a multi-part series demonstrating multithreading techniques and performance characteristics in VB.Net. Read part one: Introduction to multithreading.
In the last post, I outlined some of the basics of multithreading. In today's installment, we will be constructing the outline of an application that will show off some different multithreading tricks, techniques, and their performance characteristics. We will be using a modular design, so that we can replace the test functionality with code from other projects to see what multithreading works best for that code.
Without going into any details (it is a very simple Windows application), we put together a basic interface that looks like this:
The only thing in this basic application that is of interest is the status bar. In the status bar, we use a call to System.Environment.ProcessorCount to report the number of logical CPUs (the screenshot was taken on an AMD Sempron system).
Internally, we are putting the processing logic into an entirely separate class as a Shared function, so that we may easily create a command line version of the application, if desired. This class, ThreadTestUtilities also contains two Enumerations for the test types:
Public Enum MultiThreadTestType
SingleThread = 1
DotNetThreadPool = 2
OnePerLogicalCPU = 3
OnePerPhysicalCPU = 4
Arbitrary = 5
Public Enum AtomicType
NonAtomic = 1
Mutex = 2
Monitor = 3
SyncLocking = 4
Although MultiThreadTestType contains the member OnePerPhysicalCPU, you will note that the screenshot does not show this option. The reason is that relying upon the physical CPU count is quite a bad idea, as explained in the previous post. But we put it into our Enumeration anyways, in case we decide later on to allow testing on that basis.
The Shared function for actually running the tests looks like this for the moment:
Public Shared Function RunTest(ByVal TestType As MultiThreadTestType, ByVal AtomicOperations As Boolean, ByVal Iterations As Integer, Optional ByVal ThreadCount As Integer = 0) AsDouble
Dim DateTimeStartTime As DateTime
Dim DoubleReturnValue As Double DateTimeStartTime = DateTime.Now DoubleReturnValue = (DateTime.Now - DateTimeStartTime).TotalMilliseconds DateTimeStartTime = Nothing
The function's return results are the total number of milliseconds that it takes for the test to run. You will note that it looks like no multithreading is occurring yet. This is on purpose. We want as little multithreading in the application outside of the test itself. We will later constructing different functions for actually running the tests and calling them in a standard procedural way from within RunTest().
The final step before we can start constructing the tests themselves is to write a baseline test function. Each of the tests will use the exact same function to perform some computations in order to keep a level playing field amongst all tests. Here is our test function:
Private Function Compute(ByVal InputValue As Double) As Double
Dim DoubleOutputValue As Double
Dim DateTimeNow As DateTime
Dim rndNumberGenerator As System.Random DateTimeNow = New DateTime(DateTime.Now.Ticks)
rndNumberGenerator = New System.Random(DateTimeNow.Hour + DateTimeNow.Minute + DateTimeNow.Millisecond) If DateTimeNow.Millisecond > 500 Then
DoubleOutputValue = System.Math.IEEERemainder(System.Math.Exp(rndNumberGenerator.Next * (InputValue + 5000) * System.Math.E), rndNumberGenerator.Next)
DoubleOutputValue = rndNumberGenerator.Next(InputValue, System.Math.Max(Integer.MaxValue, System.Math.Log(System.Math.Pow(System.Math.PI, InputValue))))
End If DateTimeNow = Nothing
rndNumberGenerator = Nothing Return DoubleOutputValue
As you can see, our test function is designed to put the processor through its paces (at least floating point operations) without using much memory at all or to require any disk access. You may want to modify this code to use a lot of memory or to perform disk access or network operations, to meet the conditions that the applications that you are working on will be facing.
In the next post in the series, we will write our first battery of tests, to show single threaded performance, with no locking mechanism required or needed. While the development machine that I am currently working on has only one logical CPU (AMD Sempron 3200, x64 architecture, single core CPU), we will get to see this code in action on an AMD Athlon 3200+ (x64 architecture, single core CPU), an Intel Pentium 4 2.8 gHz (x386 architecture, single core CPU, one logical CPU), and a dual Xeon 3.0 gHz system (two dual-core, HyperThreaded Xeon CPUs with x64 architecture for a total of eight logical CPUs). These will be our test systems for the duration of this blog series, and should let you see how identical multithreading models perform on very different systems.
Justin James is the Lead Architect for Conigent.