csammisrun

A rare situation

What thread do shaim events use? – or, Why status updates don’t make the UI cry

without comments

Fourth in the series of “how or why shaim does what it does”:

  1. WPF versus IM typing notifications
  2. WPF windows, in my Winforms application?
  3. .NET 3.0 functionality in .NET 2.0: a sorting, filtering, bindable list (Part Two)

Right on the heels of the two-parter on the BindingListCollection, this little spiel will continue the trend of describing the core contact list and its data structures, and how it all works with the UI. We’ll also touch on the threading model – or complete lack thereof – of the shaim core and its plugins.

A little GUI development background, but not much

A graphical UI (any graphical UI, not just shaim’s) can only be updated by the thread that created it. If another thread tries to update some text, or move a window, or change something’s color, the UI could deadlock and that is not good. The process of executing code on the UI thread is called invoking : any thread can call the Invoke method on a UI object, give it some code to execute, and that code will be executed on the UI thread so it can safely update the UI.

shaim’s hedonistic threading free-for-all

With one notable exception – which is the subject of this post – the shaim core doesn’t make much of an attempt to control the threading of its plugins. Each plugin is free to do whatever it wants, and shaim events aren’t guaranteed to be synchronized to anything in particular (the core does synchronize access to its data structures so that operations like ContactList.UpdateContactStatus will execute in the order they were called).

Let’s take OscarLib as an example. The initial login sequence of OSCAR is a pretty rigid call-response setup, so when the network socket receives data, it processes and sends a response on the same thread. The synchronous processing ensures that there aren’t any responses out of order from the packets that initiated them. This all changes, however, once the OSCAR session is initialized and asynchronous messages like status updates start coming in. Processing something like an incoming message may take a noticeable amount of time, and if it’s being processed on the same thread that received it, the socket might end up missing a packet. To solve this problem, the socket uses the ThreadPool.QueueUserWorkItem method to dispatch an incoming packet. The packet is processed by a random thread in the thread pool, a system-allocated collection of threads that are meant to be spawned for processing information in the background (that is, not in the main process, which in this case is the thread that owns the socket).

“Processed by a random thread” – so these events are not happening on the UI thread. Huh. Problems ahoy!

The easy part: dispatching shaim events in the UI

Each shaim event defined by the core defines a delegate that can handle itself (tee hee).

public class ShaimMessageReceivedEvent : ShaimIncomingEvent
{
    public delegate void Handler(ShaimMessageReceivedEvent);
}

When a plugin registers to handle a specific event, it uses a method that matches the signature of the event’s Handler delegate. We can define a very very simple incoming message handler registered by the UI plugin that will display the message to the shaim user:

private void HandleIncomingMessage(ShaimMessageReceivedEvent evnt)
{
    Conversation conv = FindConversation(evnt.Contact); // Gets a reference to the conversation window for this contact
    conv.AppendMessage(evnt.Message);  // Appends the text of the message to the conversation window
}

Simple, but it won’t work right. When this event comes in on a thread other than the UI thread – and it most assuredly will – the UI will lock up and WPF may even throw an exception and die unexpectedly and leave a ton of debt for its family to take care of, but it had no life insurance and the whole debacle ends in bankruptcy.

Let’s avoid that by dispatching the incoming event onto the UI thread:

// A simple delegate that will be initialized in a single method's scope
private delegate void AnonymousInvoker();

private void HandleIncomingMessage(ShaimMessageReceivedEvent evnt)
{
    AnonymousInvoker invoker = delegate
    {
        Conversation conv = FindConversation(evnt.Contact); // Gets a reference to the conversation window for this contact
        conv.AppendMessage(evnt.Message);  // Appends the text of the message to the conversation window
    };
    // Invoke the operation on the UI thread
    Application.Current.Dispatcher.BeginInvoke(invoker);
}

It’s a bit more verbose, and it has to be done for every incoming event, but it’s 100% thread-safe. It also has the benefit of removing the responsibility of marshalling every event onto the UI thread from the core. This is a desirable trait because it encapsulates UI responsibility within the UI plugin. The shaim framework might not even have an active UI plugin, so there’d be no reason for the core to have to perform the extra logic.

The hard part: dispatching binding events in the core

The above works well when the developer is defining their own event handlers, but it doesn’t solve the thread dispatch problem when something happens for which there isn’t an explicit shaim event. When there’s a direct binding between a data structure and the UI, and another thread updates the data structure, the binding attempts to update on the same thread that invoked the change and the UI craps itself. This presents a huge problem for the contact list and its constituent objects and collections, all of which are directly bound by the UI and updated by random threads spewing out of the plugins.

Well hell, there goes our fever-dream of the core not knowing anything about dispatching onto the UI. Fortunately, we can at least design the solution so that the core doesn’t have to know anything specific about the UI. It has to know two things: one, that the UI requires dispatching, and two, how to dispatch an event onto the UI thread. In turn, the UI has to expose information about whether it requires dispatching, and provide a generic mechanism for doing dispatch.

An interface is defined that provides a method to check whether if dispatch is required, and a method to do the dispatch:

/// <summary>
/// Specifies an interface for an object that can dispatch method calls onto the UI thread
/// </summary>
public interface IUIDispatcher
{
    /// <summary>
    /// Dispatches a delegated method onto the UI thread
    /// </summary>
    /// <param name="methodDelegate">The delegate to invoke on the UI thread</param>
    /// <param name="parameters">A list of parameters to pass to the method</param>
    void Dispatch(Delegate methodDelegate, params object[] parameters);
    /// <summary>
    /// Checks to see if the currently executed thread has access to the UI thread
    /// </summary>
    /// <returns><c>true</c> if a method can be executed in the context of the UI thread</returns>
    bool CheckAccess();
}

…and the UI plugin interface is extended to request one of these buggers from the UI plugin:

public interface IUIPlugin
{
    ...
    /// <summary>
    /// Gets a <see cref="IUIDispatcher"/> object to delegate method calls to the UI thread
    /// </summary>
    /// <returns>A new <see cref="IUIDispatcher"/></returns>
    IUIDispatcher CreateDispatcher();
}

Each UI plugin will define their IUIDispatcher differently, but the WPF plugin does it like so:

public class UIDispatcher : IUIDispatcher
{
    private DispatcherPriority dispatchPriority;

    /// <summary>
    /// Initializes a new UIDispatcher
    /// </summary>
    /// <param name="dispatcherPriority">The priority at which to dispatch method calls</param>
    public UIDispatcher(DispatcherPriority dispatcherPriority)
    {
        this.dispatchPriority = dispatcherPriority;
    }

    #region IUIDispatcher Members

    public void Dispatch(Delegate methodDelegate, params object[] parameters)
    {
        if (Application.Current != null)
        {
            if (parameters.Length > 0)
            {
                Application.Current.Dispatcher.Invoke(dispatchPriority, methodDelegate, parameters[0]);
            }
            else
            {
                Application.Current.Dispatcher.Invoke(dispatchPriority, methodDelegate);
            }
        }
    }

    public bool CheckAccess()
    {
        return Application.Current.Dispatcher.Thread == System.Threading.Thread.CurrentThread;
    }

    #endregion
}

Using the IUIDispatcher in the BindingListCollection

Let’s take a look back at the BindingListCollection. It raises binding events in response to things like adding or removing items, as well as moving items around in response to status or name updates. Since those updates may be coming from some crazy random thread, it’ll need to use the IUIDispatcher to raise those events. The following is pseudocodeish, but it illustrates the point; consult the actual BindingListCollection code for details on the move operation and how the IUIDispatcher gets assigned.

private IUIDispatcher uiDispatcher;
...
private delegate void MoveInListHandler(T item);

void MoveInList(T item)
{
    // If there is no UI dispatcher or we're on the right thread...
    if(uiDispatcher == null || uiDispatcher.CheckAccess())
    {
        // Do the MoveInList action, finding new indexes and moving the item
    }
    else
    {
        // There is a UI dispatcher to contend with, and we're on the wrong thread
        // Invoke the current method on the UI thread, then the above code will execute correctly
        uiDispatcher.Dispatch(new MoveInListHandler(MoveInList), item);
    }
}

Presto! This method is used throughout the BindingListCollection and the ContactList to ensure directly bound data members are updated on the correct thread, and no UI plugin has to be concerned about where updates are coming from.

Ensuring events are invoked correctly is a pain in the ass, but a wholly necessary one. The IUIDispatcher is the best method I could design, but if any readers have ideas about how shaim could institute a threading model that doesn’t involve *every* plugin operation being executed on the UI thread, I’d love to hear about it in the comments.

Written by Chris

July 25th, 2008 at 10:03 am

Leave a Reply