Drivers and Multi-Threading
Client applications that use ASCOM drivers are becoming increasingly sophisticated in their use of multiple threads to provide a comprehensive and responsive user experience. This being the case, drivers authors have to ensure that their drivers will perform well if used by such applications. So, what does this mean for a driver author?
The key question to ask yourself is what will happen if two or more of your properties or methods are called at the same time? For each of these you should be clear about whether it can function independently or whether it has dependencies. In many cases the communication channel to the hardware that you are driving will be such a dependency and this will definitely be the case if you use a serial port as these can generally do just one thing at a time!
The ASCOM Serial component has a measure of protection built in which prevents multiple simultaneous use of the serial port by multi-threaded applications when using its transmit and receive mehtods. This however is unlikely to be sufficient for your needs as in many cases you will need to transmit something to your device and then wait for its reply while blocking any attempts by other threads to interrupt your "transmit - receive" transaction.
In the .NET world there are a variety of mechanics including Mutexes, Monitors and Semaphores each with its own pros and cons. Mutexes are a good place to start if you are unsure. In other languages you will need to check out what synchronisation features are provided.
The example below shows how to protect a transmit - receive transaction with a mutex. The key synchronisation lines are SerMutex.WaitOne and SerMutex.ReleaseMutex in the SerialWorker class. The rest of the code is to create the multiple threads in order to demonstrate that the mutex works! If you do try this out you should find that each command receives the response that it is expecting. If you comment out the two SerMutex lines, you will find that from time to time a thread will pick up the response from another thread because that second thread just happend to get in between the SerPort.Transmit and SerPort.ReceiveTerminated commands of the first thread.
This is impossible when the mutexes are in place because although the seond thread starts, it gets hung up on its SerMutex.WaitOne until the first thread releases the mutex.
CommandString and CommandBool are specific (but are unlikely to be the only cases) where you should use synchronisation to ensure that you get the correct response to the command that you sent.
'This example is advanced and demonstrates in the SerWorker class how to use a Mutex to protect 'a serial transmit - receive transaction. Sub ThreadingExample is the main controller that starts the threads 'To run this example you will need to customise the serial portname for the device that you are talking to 'and the commands and expected responses. The example commands and responses are for the Gemini level 4 controller 'Main thread class Public Class SerialThreadingExamples 'This example starts three threads t1 to 3 each running an instanace of the SerWorker class, SerWorker1 to 3 'Each SerWork instance has different values set for the command to be sent, the expected result and its instance number 'A common serial port SerPort is created along with a common Mutex to synchronise access to the serial port 'so that the threads will access the serial port one at a time. Sub ThreadingExample() 'Define the threads and SerWorker instances Dim t1, t2, t3 As System.Threading.Thread Dim SerWorker1, SerWorker2, SerWorker3 As New SerialWorker 'Create the Mutex, Serial Port and a trace logger instance to record the main thread output. Dim Mutex As New System.Threading.Mutex Dim SerPort As New ASCOM.Utilities.Serial Dim TLMain As New ASCOM.Utilities.TraceLogger("", "SerialMainThread") Try 'Set up the serial port and tracelogger SerPort.PortName = "COM1" SerPort.Connected = True TLMain.Enabled = True 'Create the threads and point them at the three serworker instances t1 = New System.Threading.Thread(AddressOf SerWorker1.SerialWork) t2 = New System.Threading.Thread(AddressOf SerWorker2.SerialWork) t3 = New System.Threading.Thread(AddressOf SerWorker3.SerialWork) 'Set up the commands and responses for the three serworker instances SerWorker1.Command = ":GVP#" SerWorker1.Response = "Losmandy Gemini#" SerWorker1.SerPort = SerPort SerWorker1.Instance = 1 SerWorker1.SerMutex = Mutex SerWorker2.Command = ":GVN#" SerWorker2.Response = "4.10#" SerWorker2.SerPort = SerPort SerWorker2.Instance = 2 SerWorker2.SerMutex = Mutex SerWorker3.Command = ":GV#" SerWorker3.Response = "410#" SerWorker3.SerPort = SerPort SerWorker3.Instance = 3 SerWorker3.SerMutex = Mutex 'Start the threads and wait a quarter of a second between each start t1.Start() System.Threading.Thread.Sleep(250) t2.Start() System.Threading.Thread.Sleep(250) t3.Start() 'Now send this main thread to sleep for 5 seconds while the three worker threads do their work. 'The 500 commands per worker take about 4 seconds in total to process so are finished before 'the main thread wakes up from its 5 second sleep TLMain.LogMessage("SerialThread", "Starting sleep") System.Threading.Thread.Sleep(5000) TLMain.LogMessage("SerialThread", "Finished sleep") 'Clean up the serial port and trace logger SerPort.Connected = False SerPort.Dispose() SerPort = Nothing TLMain.Enabled = False TLMain.Dispose() TLMain = Nothing Catch ex As Exception TLMain.LogMessage("SerialThread", "Exception " & ex.ToString) End Try End Sub End Class 'Worker thread class Public Class SerialWorker Public Command As String Public Response As String Public Instance As Integer Public SerPort As ASCOM.Utilities.Serial 'Serial port to use Public SerMutex As System.Threading.Mutex 'Semaphore to use 'Worker class that will run on a thread and undertake 500 transmit - receive transactions 'checking each time that the received response is the expected response. Public Sub SerialWork() 'Create a trace logger to log information Dim TL As New ASCOM.Utilities.TraceLogger("", "SerialThread " & Instance.ToString) Dim SerResponse As String Try 'Enable the trace logger and report the given parameters TL.Enabled = True TL.LogMessage("Command", Command) TL.LogMessage("Response", Response) TL.LogMessage("Instance", Instance) For i = 1 To 500 TL.LogStart("SerialThread " & Instance, "Loop " & i.ToString & " ") 'Start the transaction by waiting until the mutex is granted SerMutex.WaitOne() 'Now we have the mutex so send the command and wait for its response SerPort.Transmit(Command) SerResponse = SerPort.ReceiveTerminated("#") 'Release the mutex so the next thread can start SerMutex.ReleaseMutex() 'Log the response and compare it to what is expected. TL.LogFinish(SerResponse) If SerResponse <> Response Then TL.LogMessage("*****", "Response not what was expected: " & SerResponse & " " & Response) End If Next Catch ex As Exception TL.LogFinish(" " & ex.ToString) End Try 'Clean up the trace logger Try : TL.Enabled = False : Catch : End Try Try : TL.Dispose() : Catch : End Try TL = Nothing End Sub End Class