Chapter 9 (Performance and Multithreading) Samples
Listing 9.1. Code to Manage Single-Task Execution on a Background Thread
Option Strict On Imports System Public Class ThreadExecuteTask 'States we can be in. Public Enum ProcessingState '---------------- 'Initial state '---------------- 'Not doing anything interesting yet notYetStarted '---------------- 'Working states '---------------- 'We are waiting for the background thread to start waitingToStartAsync 'Code is running in the background thread running 'Requesting that the calculation be aborted requestAbort '---------------- 'Final states '---------------- 'Final State: We have successfully completed background 'execution done 'Final State: We have aborted the background execution 'before finishing aborted End Enum Private m_processingState As ProcessingState Public Delegate Sub ExecuteMeOnAnotherThread(ByVal _ checkForAborts As ThreadExecuteTask) Private m_CallFunction As ExecuteMeOnAnotherThread Private m_useForStateMachineLock As Object Public Sub New(ByVal functionToCall As ExecuteMeOnAnotherThread) 'Create an object we can use for a lock for the 'state machine transition function m_useForStateMachineLock = New Object 'Mark our execution as ready to start m_processingState = ProcessingState.notYetStarted 'Store the function we are supposed to call on the new 'thread m_CallFunction = functionToCall '------------------------------------------------------ 'Create a new thread and have it start executing on: ' this.ThreadStartPoint() '------------------------------------------------------ Dim threadStart As System.Threading.ThreadStart threadStart = _ New System.Threading.ThreadStart(AddressOf ThreadStartPoint) Dim newThread As System.Threading.Thread newThread = New System.Threading.Thread(threadStart) 'Mark our execution as ready to start (for determinism, 'it is important to do this before we start the thread!) setProcessingState(ProcessingState.waitingToStartAsync) 'Tell the OS to start our new thread async. newThread.Start() 'Return control to the caller on this thread End Sub '------------------------------------------------------- 'This function is the entry point that is called on the 'new thread '------------------------------------------------------- Private Sub ThreadStartPoint() 'Set the processing state to indicate we are running on 'a new thread! setProcessingState(ProcessingState.running) 'Run the user's code, and pass in a pointer to our class 'so that code can occasionally call to see if an abort has 'been requested m_CallFunction(Me) 'If we didn't abort, change the execution state to indicate 'success If (m_processingState <> ProcessingState.aborted) Then 'Mark our execution as done setProcessingState(ProcessingState.done) End If 'Exit the thread... End Sub '-------------------------------------------------- 'The state machine. '-------------------------------------------------- Public Sub setProcessingState(ByVal nextState As _ ProcessingState) 'We should only allow one thread of execution to try 'to modify the state at any given time. SyncLock (m_useForStateMachineLock) 'If we are entering the state we are already in, 'do nothing. If (m_processingState = nextState) Then Return End If '-------------------------------------------------- 'Some very simple protective code to make sure 'we can't enter another state if we have either 'successfully finished, or successfully aborted '-------------------------------------------------- If ((m_processingState = ProcessingState.aborted) _ OrElse (m_processingState = ProcessingState.done)) Then Return End If 'Make sure the state transition if valid Select Case (nextState) Case ProcessingState.notYetStarted Throw New Exception _ ("Cannot enter 'notYetStarted' state") Case ProcessingState.waitingToStartAsync If (m_processingState <> ProcessingState.notYetStarted) _ Then Throw New Exception("Invalid state transition") End If Case ProcessingState.running If (m_processingState <> _ ProcessingState.waitingToStartAsync) Then Throw New Exception("Invalid state transition") End If Case ProcessingState.done 'We can complete work only if we have been running. 'It is also possible that the user requested an 'abort, but we finished the work before aborting If ((m_processingState <> ProcessingState.running) _ AndAlso _ (m_processingState <> ProcessingState.requestAbort)) Then Throw New Exception("Invalid state transition") End If Case ProcessingState.aborted If (m_processingState <> _ ProcessingState.requestAbort) Then Throw New Exception("Invalid state transition") End If End Select 'Allow the state change m_processingState = nextState End SyncLock End Sub Public ReadOnly Property State() As ProcessingState Get Dim currentState As ProcessingState 'Prevents simultanious read/write of the state variable SyncLock (m_useForStateMachineLock) currentState = m_processingState End SyncLock Return currentState End Get End Property End Class
Listing 9.2. Test Example for Work to Be Done on a Background Thread
Option Strict On Imports System '--------------------------------------------------------- 'Test code we will use to try background thread execution '--------------------------------------------------------- Public Class Test1 Public m_loopX As Integer '------------------------------------------------------------ 'The function that gets called on a background thread ' ' [in] threadExecute: The class managing our thread's ' execution. We can check this to see if ' we should abort our calculation. '--------------------------------------------------------------- Public Sub ThreadEntryPoint(ByVal threadExecute As _ ThreadExecuteTask) 'This message box will be shown in the context of the thread 'it is running in MsgBox("In TEST") '------------------------------ '60 times '------------------------------ For m_loopX = 1 To 60 'If an abort has been requested, we should quit If (threadExecute.State = _ ThreadExecuteTask.ProcessingState.requestAbort) Then threadExecute.setProcessingState( _ ThreadExecuteTask.ProcessingState.aborted) Return End If 'Simulate work: Wait 1/3 second System.Threading.Thread.Sleep(333) Next End Sub End Class
Listing 9.3. Code to Test and Run the Sample Code Above
[View full width]
'The class that will manage our new thread's execution Private m_threadExecute As ThreadExecuteTask 'The class with the method we want to run async Private m_testMe As Test1 '------------------------------------------------------- 'This code needs to be run before the other code because 'it starts the background execution! ' 'Create a new thread and get the execution going '------------------------------------------------------- Private Sub buttonStartAsyncExecution_Click(ByVal sender _ As System.Object, ByVal e As System.EventArgs) _ Handles buttonStartAsyncExecution.Click 'Create an instance of the class we 'want to call a method on, in another thread m_testMe = New Test1 'Package the class' method entry point up in a delegate Dim delegateCallCode As _ ThreadExecuteTask.ExecuteMeOnAnotherThread delegateCallCode = _ New ThreadExecuteTask.ExecuteMeOnAnotherThread(AddressOf _ m_testMe.ThreadEntryPoint) 'Tell the thread to get going! m_threadExecute = New ThreadExecuteTask(delegateCallCode) End Sub 'Cause an illegal state transition (will raise an exception) Private Sub buttonCauseException_Click(ByVal sender As _ System.Object, ByVal e As System.EventArgs) _ Handles buttonCauseException.Click m_threadExecute.setProcessingState( _ ThreadExecuteTask.ProcessingState.notYetStarted) End Sub 'Request the async code to abort its work Private Sub buttonAbort_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles buttonAbort.Click m_threadExecute.setProcessingState( _ ThreadExecuteTask.ProcessingState.requestAbort) End Sub 'Check the status of our execution Private Sub buttonCheckStatus_Click(ByVal sender As System.Object _ ByVal e As System
.EventArgs) Handles ButtonCheckStatus.Click 'Ask the thread management class what state it's in MsgBox(m_threadExecute.State.ToString()) 'Ask the class with the method running on the thread how it's progressing MsgBox(m_testMe.m_loopX.ToString()) End Sub
Listing 9.4. Code That Goes into the Smartphone Form1.cs Class
'---------------------------------------------------- 'All this code belongs inside a Form1.cs class '---------------------------------------------------- 'The object that will do our background calculation Private m_findNextPrimeNumber As FindNextPrimeNumber '--------------------------------------------------------- 'Update the status text... '--------------------------------------------------------- Sub setCalculationStatusText(ByVal text As String) Label1.Text = text End Sub Private Sub menuItemExit_Click(ByVal sender As _ System.Object, ByVal e As System.EventArgs) _ Handles menuItemExit.Click Me.Close() End Sub '--------------------------------------------------------- 'Menu item for starting the background calculation '--------------------------------------------------------- Private Sub menuItemStart_Click(ByVal sender As _ System.Object, ByVal e As System.EventArgs) _ Handles menuItemStart.Click 'What number do we want to start looking at Dim startNumber As Long = _ System.Convert.ToInt64(TextBox1.Text) 'Set up the background calculation m_findNextPrimeNumber = New FindNextPrimeNumber(startNumber) 'Start the background processing running... m_findNextPrimeNumber.findNextHighestPrime_Async() 'Set up the timer that will track the calculation Timer1.Interval = 400 '400 ms Timer1.Enabled = True End Sub '--------------------------------------------------------- 'Menu item for "Aborting" a calculation under progress '--------------------------------------------------------- Private Sub menuItemAbort_Click(ByVal sender As _ System.Object, ByVal e As System.EventArgs) _ Handles menuItemAbort.Click 'If we are not doing a calculation, do nothing. If (m_findNextPrimeNumber Is Nothing) Then Return 'Set the thread up to abort m_findNextPrimeNumber.setProcessingState( _ FindNextPrimeNumber.ProcessingState.requestAbort) 'Let the user instantly know we are getting 'ready to abort... setCalculationStatusText("Waiting to abort..") End Sub '------------------------------------------------------- 'This timer gets called on the UI thread and enables 'us to keep track of progress on our background 'calculation '------------------------------------------------------- Private Sub Timer1_Tick(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Timer1.Tick 'If we get called and we have no prime number 'we are looking for, turn off the timer If (m_findNextPrimeNumber Is Nothing) Then Timer1.Enabled = False Return End If '-------------------------------------------- 'If we've been aborted, throw out the prime seeker 'and turn off the timer '-------------------------------------------- If (m_findNextPrimeNumber.getProcessingState = _ FindNextPrimeNumber.ProcessingState.aborted) Then Timer1.Enabled = False m_findNextPrimeNumber = Nothing setCalculationStatusText("Prime search aborted") Return End If '-------------------------------------------- 'Did we find the right answer? '-------------------------------------------- If (m_findNextPrimeNumber.getProcessingState = _ FindNextPrimeNumber.ProcessingState.foundPrime) Then Timer1.Enabled = False 'Show the result setCalculationStatusText("Found! Next Prime = " + _ m_findNextPrimeNumber.getPrime().ToString()) m_findNextPrimeNumber = Nothing Return End If '-------------------------------------------- 'The calculation is progressing. Give the 'user an idea of the progress being made... '-------------------------------------------- 'Get the two output values Dim numberCalculationsToFar As Long Dim currentItem As Long m_findNextPrimeNumber.getExecutionProgressInfo( _ numberCalculationsToFar, currentItem) setCalculationStatusText("In progress. Looking at: " + _ CStr(currentItem) + ". " + _ CStr(numberCalculationsToFar) + _ " calculations done for you so far!") End Sub
Listing 9.5. Code for the FindNextPrimeNumber.cs Class
Option Strict On Imports System Public Class FindNextPrimeNumber 'States we can be in. Public Enum ProcessingState notYetStarted waitingToStartAsync lookingForPrime foundPrime requestAbort aborted End Enum Private m_startPoint As Long Private m_NextHighestPrime As Long 'How many items have been searched? Private m_comparisonsSoFar As Long 'What is the current item we are doing a prime search for? Private m_CurrentNumberBeingExamined As Long 'Called to get an update on how the calculation is progressing Public Sub getExecutionProgressInfo( _ ByRef numberCalculationsSoFar As Long, _ ByRef currentItemBeingLookedAt As Long) 'NOTE: We import this thread lock to make sure that 'we are not reading these values while they are in the 'middle of being written out. Because 'm_comparisonsSoFar' 'and 'm_CurrentNumberBeingExamined' may be 'accessed from multiple threads, any read or write 'operation to them needs to be synchronized with "lock" to 'ensure that reads and writes are atomic. SyncLock (Me) numberCalculationsSoFar = m_comparisonsSoFar currentItemBeingLookedAt = m_CurrentNumberBeingExamined End SyncLock End Sub Private m_processingState As ProcessingState '-------------------------------------------------- 'A very simple state machine. '-------------------------------------------------- Public Sub setProcessingState(ByVal nextState As _ ProcessingState) '-------------------------------------------------- 'Some very simple protective code to make sure 'we can't enter another state if we have either 'successfully finished, or successfully aborted '-------------------------------------------------- If ((m_processingState = ProcessingState.aborted) _ OrElse (m_processingState = ProcessingState.foundPrime)) _ Then Return End If 'Allow the state change m_processingState = nextState End Sub Public ReadOnly Property getProcessingState() As ProcessingState Get Return m_processingState End Get End Property '--------------------------------------------------- 'Returns the prime '--------------------------------------------------- Public Function getPrime() As Long If (m_processingState <> ProcessingState.foundPrime) Then Throw New Exception("Prime number not calculated yet!") End If Return m_NextHighestPrime End Function 'Class constructor Public Sub New(ByVal startPoint As Long) setProcessingState(ProcessingState.notYetStarted) m_startPoint = startPoint End Sub '------------------------------------------------------------ 'Creates a new worker thread that will call ' "findNextHighestPrime()" '------------------------------------------------------------ Public Sub findNextHighestPrime_Async() Dim threadStart As System.Threading.ThreadStart threadStart = _ New System.Threading.ThreadStart(AddressOf _ findNextHighestPrime) Dim newThread As System.Threading.Thread newThread = New System.Threading.Thread(threadStart) 'Set our processing state to say that we are looking setProcessingState(ProcessingState.waitingToStartAsync) newThread.Start() End Sub '------------------------------------------------------------ 'This is the main worker-function. This synchronously starts 'looking for the next prime number and does not exit until 'either: ' (a) The next prime is found ' (b) An external thread to this thread tells us to abort '------------------------------------------------------------ Public Sub findNextHighestPrime() 'If we've been told to abort, don't even start looking If (m_processingState = ProcessingState.requestAbort) Then GoTo finished_looking End If 'Set our processing state to say that we are looking setProcessingState(ProcessingState.lookingForPrime) Dim currentItem As Long 'See if it's odd If ((m_startPoint And 1) = 1) Then 'It's odd, start at the next odd number currentItem = m_startPoint + 2 Else 'It's even, start at the next odd number currentItem = m_startPoint + 1 End If 'Look for the prime item. While (m_processingState = ProcessingState.lookingForPrime) 'If we found the prime item, return it If (isItemPrime(currentItem) = True) Then m_NextHighestPrime = currentItem 'Update our state setProcessingState(ProcessingState.foundPrime) End If currentItem = currentItem + 2 End While finished_looking: 'Exit. At this point we have either been 'Told to abort the search by another thread, or 'we have found and recorded the next highest prime number 'If an abort was requested, note that we have aborted 'the process. If (m_processingState = ProcessingState.requestAbort) Then setProcessingState(ProcessingState.aborted) End If End Sub 'Helper function that looks to see if a specific item 'is a prime number. Private Function isItemPrime(ByVal potentialPrime _ As Long) As Boolean 'If it's even, it's not prime If ((potentialPrime And 1) = 0) Then Return False End If 'We want to look up until just past the square root 'of the item Dim end_point_of_search As Long end_point_of_search = _ CLng(System.Math.Sqrt(potentialPrime)) + 1 Dim current_test_item As Long = 3 While (current_test_item <= end_point_of_search) '--------------------------------- 'Check to make sure we have not been asked to abort! '--------------------------------- If (m_processingState <> ProcessingState.lookingForPrime) _ Then Return False End If 'If the item is divisible without remainder, 'it is not prime If (potentialPrime Mod current_test_item = 0) Then Return False End If 'advance by two current_test_item = current_test_item + 2 '------------------------------------------------------ 'Up the count of items we have examined '------------------------------------------------------ 'NOTE: We import this thread lock to make sure that 'we are not reading these values while they are in the 'middle of being written out. Because 'm_comparisonsSoFar' 'and 'm_CurrentNumberBeingExamined' may be 'accessed from multiple threads, any read or write 'operation to them needs to be synchronized with "lock" to 'ensure that reads and writes are atomic. SyncLock (Me) m_CurrentNumberBeingExamined = potentialPrime m_comparisonsSoFar = m_comparisonsSoFar + 1 End SyncLock End While 'The item is prime Return True End Function End Class
|