Messaging between Threads using Message Loop


Introduction

MessageLoopLib is a stripped down version of a complete, threading communication subsystem Ive 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.

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. Main entry point in TestMessageLoopForm.cs

    static void Main()
    {
    try
    {
    Thread.CurrentThread.Name = "Main App Thread";
    Application.Run(
    new MessageLoopForm());
    }
    catch( Exception exc)
    {
    //This is from another library -- ExceptionInfoLib -- if you haven't downloaded it, comment out the call and "using" statement.
    ExceptionInfoDisplay.DisplayInfo( exc);
    }
    }

  2. To receive messages from a thread the client must inherit and implement IMessageLoop (MessageInterface.cs) interface, which consists of three methods.

    ThreadStartedProc( object sender, MessagePacket msg);
    ThreadMessageProc(
    object sender, MessagePacket msg);
    ThreadClosingProc(
    object sender, MessagePacket msg);

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

    public void ThreadStartedProc( object sender, MessagePacket msg) or
    void IMessageLoop.ThreadStartedProc( object sender, MessagePacket msg)

    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)

    private delegate void ThreadStartedEventHandler( object sender, MessagePacket msg);
    private event ThreadStartedEventHandler OnThreadStartedEvent;
    public MessageLoop( object consumer)
    {
    try
    {
    this.consumer = (IMessageLoop)consumer;
    }
    catch( Exception exc)
    {
    MessageLoopException loopExc =
    null;
    loopExc =
    new MessageLoopException( "IMessageLoop interface must be implemented", exc);
    throw( loopExc);
    }
    OnThreadStartedEvent =
    new ThreadStartedEventHandler( this.consumer.ThreadStartedProc);
    OnThreadMessageEvent =
    new ThreadMessageEventHandler( this.consumer.ThreadMessageProc);
    OnThreadClosingEvent =
    new ThreadClosingEventHandler( this.consumer.ThreadClosingProc);
    }

    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 hasnt 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 void Start()
    {
    if( thread == null)
    {
    CreateThread();
    }
    if( thread.IsAlive == false)
    {
    waitOnStart =
    new AutoResetEvent( false);
    thread.Start();
    waitOnStart.WaitOne();
    waitOnStart.Close();
    waitOnStart =
    null;
    }
    else
    {
    MessageLoopException exc =
    null;
    exc =
    new MessageLoopException( "Thread " + this.Name + " is already running.");
    throw( exc);
    }
    }

    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 ethod doesnt return until the message procedure has been entered. Finally the event is losed. If "Start" is called while the thread is running, an exception is thrown.

  6. The Send & Post methods

    public void SendMessage( MessagePacket msg)
    {
    CheckIdValue( msg);
    if( msg.MsgId == MessageId.MsgQuit)
    {
    PostMessage( msg);
    }
    else
    {
    lock( msgList)
    {
    msgList.Insert( 0, msg);
    }
    waitOnSend.WaitOne();
    }
    }
    public void PostMessage( MessagePacket msg)
    {
    CheckIdValue( msg);
    lock( msgList)
    {
    msgList.Add( msg);
    }
    if( msg.MsgId == MessageId.MsgQuit)
    {
    thread.Join();
    thread =
    null;
    }
    }

    Three points to comment on in these methods. The ArrayList is locked to revent multiple threads from accessing it simultaneously. In the Send method,  quit message is passed onto Post so all messages are dispatched before the tread is shutdown. To emulate the real world, Send waits until its message as 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 void ThreadMessageLoop()
    {
    waitOnStart.Set();
    DispatchStart();
    while( true)
    {
    MessagePacket msg =
    null;
    msg = GetMessage();
    if( msg != null)
    {
    if( msg.MsgId == MessageId.MsgQuit)
    {
    break;
    }
    else
    {
    DispatchMessage( msg);
    waitOnSend.Set();
    }
    }
    //normally set to 30 - to demonstrate send messages
    Thread.Sleep( 300);
    }
    DispatchClosing();
    }

    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. A waiting message is retrieved by the dispatcher shipped to the client. If the client thread is waiting, waitOnSend.Set() triggers its return.

  8. Application shutdown (MessageLoopForm.cs)

    protected override void OnClosing( CancelEventArgs evt)
    {
    if( btnStart.Enabled == false)
    {
    btnStopPost.PerformClick();
    }
    }

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 applications thread to exit, while the MessageLoop thread would merrily continue looping, causing the application to hang.

This article is updated to RTM by Zhanbo Sun.


Similar Articles