Messaging Between Threads using Message Loop in VB.NET

Introduction

MessageLoopLib is a stripped down version of a complete, threading communication subsystem I've written. This implementation is a single thread created in the GUI constructor. I've dropped all thread management and have had to change some of the message code to accommodate this.

messageloop-in-vb.net.jpg

Usage

The "Start Thread" button is the only control enabled at startup. Clicking it starts a thread and enables the other controls, while disabling itself. 

Scope

The code uses threading, synchronization, interfaces, delegates and events. All of which are discussed below. 

1. M
ain entry point in TestMessageLoopForm.vb

Shared Sub Main()
Try
Thread.CurrentThread.Name = "Main App Thread"
Application.Run(
New MessageLoopForm)
Catch exc As Exception
'This is from another library -- ExceptionInfoLib -- if you haven't downloaded it, comment out the call and "using" statement.
ExceptionInfoDisplay.DisplayInfo(exc)
End Try
End
 Sub 'Main

2. To receive messages from a thread the client must inherit and implement

IMessageLoop (MessageInterface.cs) interface, which consists of three methods.

ThreadStartedProc( sender as Object, msg MessagePacket)
ThreadMessageProc( sender 
as Object, msg MessagePacket)
ThreadClosingProc( sender 
as Object, msg MessagePacket)

3. The Interfaces can be defined in one of two ways

Public Sub ThreadStartedProc(ByVal sender As Object, ByVal msg As MessagePacket)
IMessageLoop.ThreadStartedProc(sender 
as Object, msg As MessagePacket)

I choose the latter method. This makes the method available only through the interface and somewhat hides the implementation. In the case of multiple interface inheritance, where a method in each share the same name and parameters, this is the only means of implementation. 

4. The MessageLoop Constructor (MessageLoop.cs)

Public Sub New(ByVal consumer As Object)
Try
Me
.consumer = CType(consumer, IMessageLoop)
Catch exc As Exception
Dim loopExc As MessageLoopException = Nothing
loopExc = New MessageLoopException("IMessageLoop interface must be implemented", exc)
Throw loopExc
End Try
OnThreadStartedEvent = New ThreadStartedEventHandler Me.consumer.ThreadStartedProc)
OnThreadMessageEvent = 
New ThreadMessageEventHandler(Me.consumer.ThreadMessageProc)
OnThreadClosingEvent = 
New ThreadClosingEventHandler(Me.consumer.ThreadClosingProc)
End Sub 'New

The client creates a MessageLoop instance with a reference to itself. This accomplishes two tasks. First, in casting the consumer to IMessageLoop, a NullReferenceException is thrown if the interface hasn't been implemented. Second, in making the delegates and events private, messageloop ensures that there not multiple callbacks attached to a delegate. Note that there are two alternative ways to determine if the IMessageLoop interface has been implemented. this.consumer = consumer as IMessageLoop - returns a "null" if not implemented. consumer is IMessageLoop - returns "false" if not implemented.

5. The Start method

Public Sub Start()
If thread Is Nothing Then
CreateThread()
End If
If
 thread.IsAlive = False Then
waitOnStart = New AutoResetEvent(False)
thread.Start()
waitOnStart.WaitOne()
waitOnStart.Close()
waitOnStart = 
Nothing
Else
Dim
 exc As MessageLoopException = Nothing
exc = New MessageLoopException("Thread " + Me.Name + " is already running.")Throw exc
End If
End
 Sub 'Start

A new thread is created on each start. Attempting to reuse a thread that has received a quit message results in a ThreadStateException. An AutoResetEvent is created so the method doesn't return until the message procedure has been entered. Finally the event is closed. If "Start" is called while the thread is running, an exception is thrown.

6. The Send & Post methods

Public Sub SendMessage(ByVal msg As MessagePacket)
CheckIdValue(msg)
If msg.MsgId = MessageId.MsgQuit Then
PostMessage(msg)
Else
SyncLock
 msgList
msgList.Insert(0, msg)
End SyncLock
waitOnSend.WaitOne()
End If
End
 Sub 'SendMessage

Public
 Sub PostMessage(ByVal msg As MessagePacket)
CheckIdValue(msg)
SyncLock msgList
msgList.Add(msg)
End SyncLock
If
 msg.MsgId = MessageId.MsgQuit Then
thread.Join()
thread = 
Nothing
End
 If
End
 Sub 'PostMessage

Three points to comment on in these methods. The ArrayList is locked to prevent multiple threads from accessing it simultaneously. In the "Send" method, a quit message is passed onto "Post" so all messages are dispatched before the thread is shutdown. To emulate the real world, "Send" waits until its message has been dispatched. The "thread.Join()", in the "Post" method, is only used in this example to prevent the application from exiting before a running thread. In the real world "Post" returns immediately.  

7. The Dispatch loop

Private Sub ThreadMessageLoop()
waitOnStart.Set()
DispatchStart()
While True
Dim
 msg As MessagePacket = Nothing
msg = GetMessage()
If Not (msg Is Nothing) Then
If
 msg.MsgId = MessageId.MsgQuit Then
Exit
 While
Else
DispatchMessage(msg)
waitOnSend.Set()
End If
End
 If
'normally set to 30 - to demonstrate send messages 
Thread.Sleep(300)
End While
DispatchClosing()
End Sub 'ThreadMessageLoop

Upon entering the dispatcher, the AutoResetEvent is signaled, permitting the "Start" method to return. After sending a start thread notification to the client, the dispatcher continues to loop until it receives a quit message. The dispatcher shipped to the client retrieves a waiting message. If the client thread is waiting, waitOnSend.Set() triggers its return. 

8. Application shutdown (MessageLoopForm.cs)

Protected Overrides Sub OnClosing(ByVal evt As CancelEventArgs)
If btnStart.Enabled = False Then
btnStopPost.PerformClick()
End If
End
 Sub 'OnClosing

If the application is closing and the Start button is disabled, then a thread is running and has to be closed. Ignoring this would cause the application's thread to exit, while the MessageLoop thread would merrily continue looping, causing the application to hang.


Similar Articles