Thursday, December 11, 2008

Delayed Event Handlers and System.Threading.SynchronizationContext

I've found myself in situations where I'm on the receiving end of an API that for whatever reason will raise events in (relative) rapid succession.  It just so happens that I need to execute some code when that event is raised, but not with every event raise. 

For example, imagine you have a graphical mapping API.  Suppose that when the user pans the map, a "Panned" event is raised.  It turns out to be quite common that a user will pan the map over and over before they finish panning. The processing I need to perform on that Panned  event isn't terribly intensive, but it's not without cost; a cost that is too expensive to incur on every Panned event, to say nothing of the worthlessness of performing this processing before user has finished panning. 

A simple answer to that is some form of delayed event processing.  Due to different circumstances at different times I've found myself using two different types of delayed event handlers:

  1. Once the event is raised, it waits for a period of time, and then executes.
  2. Once the event is raised, it waits for a period of time to elapse in which no more of the events have been raised.

The first one is analogous to a timed-delay on a camera, or System.Windows.Forms.Timer, or System.Thread.Timer.  The second is more of "I'll wait for you to finish talking before I start talking" kind of thing. Basic implementation entails firing off an asynchronous method that performs the appropriate type of waiting, and once it's finished, the actual processing takes place. 

Before getting into the details of this implementation, let's talk about UI-thread synchronization.  If you've ever taken advantage of some thready goodness in order to fork off work, then you may have found out that you are not able to update the UI from the non-UI/main thread.  If you try to touch the UI thread, the Framework will throw an InvalidOperationException telling you: "Cross-thread operation not valid: Control 'TheTextBox' accessed from a thread other than the thread it was created on." If you've run into this, you've probably googled-up just a few pages about this exception, and as such, I will not rehash the details here. In short, ISynchronizeInvoke is your answer.  Lucky for almost all cases, System.Windows.Forms.Control implements ISynchronizeInvoke. It's common enough that in some of my code you'll see a "DoOnUIThread" method that I use in my non-UI-threaded code.  It's pretty simple, take a look:

private void DoOnUIThread(System.Threading.ThreadStart method) {
   if (this.InvokeRequired) { 
      this.Invoke(method, null);
   } else {
      method();
   }
}

Where "this" is a Control-derived control, such as a System.Windows.Forms.Form. It's pretty easy to modify this to match whatever delegate you need, and I can only assume there's a simple generic implementation that could accept any delegate type, I just haven't done that yet.  It's easy enough to use:

DoOnUIThread(delegate() {
   // My UI-bound code goes here, for example:
   _TextBox.Text = "Hello There.";
});

 

Unfortunately, there are cases where you're writing something that doesn't have access to a Control-based object.  For example, imagine you're implementing a class library that needs to do threaded operations.  In your unit tests everything seems to run great - your business objects instantiate, thread, calculate and populate.  But unless you also include tests that bind those objects to the UI, you may miss that you can easily get the "Cross-thread operation not valid:" InvalidOperationException only when you bind your business object to the UI. These situations are usually easily remedies by making your objects require an ISynchronizeInvoke object upon instantiation.  That ISynchronieInvoke object can be used to use a DoOnUIThread style method to make sure you don't run into that Cross-thread exception.

Even more unfortunate, there are cases where maybe your business objects are so entrenched, or it's simply impractical to require an ISynchronizeInvoke object to be specified upon instantiation. In these cases you're really at a loss for what to do when you need to do some work on a different thread that will eventually need to touch the UI thread.  If you can safely make the assumption that the object will normally be executing in the scope of the UI thread, then you can get your worker-threads back into the context of the UI thread by using System.Threading.SynchronizationContext.  If you make sure to grab a hold System.Threading.SychronizationContext.Current before forking off your worker thread, the worker thread will be able to get back to the UI thread to execute any UI-bound code.

Now let's look at an example of this in my implementation of a simple "timer" delayed event handler:

private System.Threading.ThreadStart _Click_DelayedEventHandler;
private System.Threading.SynchronizationContext _SynchronizationContext;
private void button1_Click(object sender, EventArgs e) {
   if (_Click_DelayedEventHandler != null) {
      return;// we're already waiting - bail out
   } else {
      // setup the 'wait until finished' anonymous method:
      _Click_DelayedEventHandler = delegate() {
         System.Threading.Thread.Sleep(1000);
      };
      AsyncCallback finishedWaiting = delegate(IAsyncResult ar) {
         // Take note that at this point you will still be executing on a threadpool thread,
         // not on the UI thread.
         SendOrPostCallback hello = delegate(object state) {
            MessageBox.Show(this, "Hello World", "Caption", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
         };
         _SynchronizationContext.Post(hello, null);

         // Make SURE to cleanup or this will be a one-trick pony
         _Click_DelayedEventHandler = null;
         _SynchronizationContext = null; // don't strictly need to set this to null...
      };

      // fire off our anonymous 'waiter' method on a thread-pool thread:
      _SynchronizationContext = SynchronizationContext.Current;
      _Click_DelayedEventHandler.BeginInvoke(finishedWaiting, null);
   }
}

To try this out, you should be able to create a new Windows Forms project, place a button on the form, hook up this method as the Click event handler. When you run, you should be able to click the button, and no matter how many times you click, you'll only get the "Hello World" message box after one second has elapsed since your first button click. So, what happens is, on the first click, it sets up a ThreadStart delegate, and executes it.  The only thing this does is wait for one second.  If any more button-clicks come in during this second, they're ignored. After the second, the ThreadStart finishes, and the 'finished' callback executes.  It's in this anonymous method that we place our real work.  In the finishedWaiting you can see the use of the UI SynchronizationContext - you create a SendOrPostCallback delegate and .Post it to the UI SynchronizationContext. At the end of finishedWaiting is very important cleanup code.  If you don't set the _Click_DelayedEventHandler back to null, this whole thing is just a one-trick pony.

 

Here's the more-useful "I'll wait for you to stop clicking" delayed event handler:

private System.Threading.ThreadStart _ButtonClick_DelayedEventHandler;
private DateTime _ButtonClick_LastChangeTime;
private object _ButtonClick_LastChangeLock = new object();
private System.Threading.SynchronizationContext _ButtonClick_SynchronizationContext;
private void button1_Click(object sender, EventArgs e) {
   if (_ButtonClick_DelayedEventHandler != null) {
      // we're already waiting...increment the 'last fired' indicator.
      lock (_ButtonClick_LastChangeLock) { _ButtonClick_LastChangeTime = DateTime.UtcNow; }
   } else {
      // setup the 'wait until finished' anonymous method:
      _ButtonClick_DelayedEventHandler = delegate() {
         // this will loop indefinately until the event hasn't been
         // raised for at least 1 second
         TimeSpan difference = TimeSpan.Zero;
         do {
            System.Threading.Thread.Sleep(100);

            DateTime lastChanged;
            lock (_ButtonClick_LastChangeLock) { lastChanged = _ButtonClick_LastChangeTime; }
            difference = DateTime.UtcNow.Subtract(lastChanged);
         } while (difference.TotalMilliseconds < 1000);
      };
      AsyncCallback finishedWaiting = delegate(IAsyncResult ar) {
         SendOrPostCallback hello = delegate(object state) {
            MessageBox.Show(this, "Hello World", "Caption", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
         };
         _ButtonClick_SynchronizationContext.Post(hello, null);


         // Make SURE to cleanup or this will be a one-trick pony.
         _ButtonClick_LastChangeTime = DateTime.MinValue;
         _ButtonClick_DelayedEventHandler = null;
         _ButtonClick_SynchronizationContext = null;
      };


      // fire off our anonymous 'waiter' method on a thread-pool thread:
      _ButtonClick_SynchronizationContext = SynchronizationContext.Current; // assumed to be the UI thread
      _ButtonClick_LastChangeTime = DateTime.UtcNow;
      _ButtonClick_DelayedEventHandler.BeginInvoke(finishedWaiting, null);
   }
}


To try this out, do the same steps as the first delayed event handler - hook this up to a button-click.  The difference you'll notice is that as long as you keep clicking the button at least once every second, the message box will not pop up.  It waits for you to finish hammering the button.  Again, it's very important that you cleanup at the end of finishedWaiting, and note that there is more to cleanup this time.

Anyway, feel free to customize to your needs, and I really hope these are useful to someone other than myself.

2 comments:

Jason Poll said...

Sorry for the ugly code-wrapping...

Jason Poll said...

Oh yeah! I almost forgot! While I was zeroing in on the SynchronizationContext, Kurt Claeys' post at http://geekswithblogs.net/claeyskurt/archive/2007/08/08/114491.aspx pointed out that I was heading in the right direction.