Archive for September, 2009

Super Light Weight Multi-threaded code with ExclusiveSection

Something that has always bugged me about the .NET Base Class Library is the absence of a threading lock that would reliably tell me whether another thread was already doing something, and if so, not to block and wait for it, but just to continue on as if nothing has happened.

This becomes super useful in many multi-threaded situations, one of the main ones being User Interface threading where the foreground thread is requesting some data via an asynchronous call. Typically in this scenario, if the user makes a selection from the UI, you want the request to be sent, but if a request is already running, to ignore the user’s selection until you’re done getting the previous requests data, then at the end you can check if there was a contention and re-execute if necessary. This results in the UI remaining responsive (as in there are no locks to wait for on the foreground thread) but still ensuring the application stays in sync with the users requests.

The example I’ll give is a Windows Forms application (although it could just as easily be a WPF or Silverlight application) that searches the web and displays the html result in a textbox. I want this to be super responsive so I’ve made the application search as the user types.

Now a simple way of approximating the threading behaviour I’m after is just checking a boolean and then setting it, like so:

Code Snippet
  1.         private bool _isSearching = false;
  2.         private void SearchQueryTextBox_TextChanged(object sender, EventArgs e)
  3.         {
  4.             SearchWebAsync(SearchQueryTextBox.Text);
  5.         }
  6.         private void SearchWebAsync(string searchText)
  7.         {
  8.             if (!_isSearching)
  9.             {
  10.                 _isSearching = true;
  11.                 WebClient client = new WebClient();
  12.                 Uri searchUrl = new Uri("http://www.google.com/search?q=" + searchText);
  13.                 client.DownloadStringCompleted += WebClient_DownloadStringCompleted;
  14.                 client.DownloadStringAsync(searchUrl);
  15.             }
  16.         }
  17.         private void WebClient_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
  18.         {
  19.             if (!e.Cancelled && e.Error == null)
  20.             {
  21.                 SearchResultsTextBox.Text = e.Result;
  22.             }
  23.             _isSearching = false;
  24.         }

The problem with this code is that it can’t tell me whether the user changed their text and clicked again while the WebClient was still performing the first search and it isn’t particularly thread safe if I wanted to fire off the search from a background thread.

What’s wrong with Monitor.TryEnter?

There are a few things that have bugged me about Monitor.TryEnter, the main one being that the thread that calls Exit must be the thread that originally called TryEnter. This is all well and good in some scenarios, but in the above example it would mean I would have to call SearchWebAsync always from the UI thread. The other obvious point about Monitor.TryEnter is it doesn’t track how many times TryEnter has been attempted for a particular lock object. Not to mention it uses locking and not Interlocked statements (which are faster so I’m told although I’ve yet to see any benchmarks which say just how much faster).

Enter the ExclusiveSection class (or should I say TryEnter?)

An ExclusiveSection is used similarly to a lock in that you create an instance and then attempt to enter that section, when you do so, the ExclusiveSection.TryEnter method will return whether you were successful or not, if not you can then simply exit, knowing that the ExclusiveSection has remembered that you asked to do so.

Code Snippet
  1.     public sealed class ExclusiveSection
  2.     {
  3.         private long _entryAttempts;
  4.         /// <summary>
  5.         /// Returns true if there was more than one call to TryEnter before exiting
  6.         /// </summary>
  7.         public bool HasContention
  8.         {
  9.             get
  10.             {
  11.                 return (Interlocked.CompareExchange(ref _entryAttempts, 0, 0) > 1);
  12.             }
  13.         }
  14.         /// <summary>
  15.         /// Ensures the caller was the first to enter the Exclusive Section (Thread-safe)
  16.         /// </summary>
  17.         /// <returns>Returns True if the Exclusive Section was entered</returns>
  18.         public bool TryEnter()
  19.         {
  20.             return (Interlocked.Increment(ref _entryAttempts) == 1);
  21.         }
  22.         /// <summary>
  23.         /// Attempt to exit, useful when exits and enters are called abitrarily,
  24.         /// ensures only one exit ever succeeds.
  25.         /// </summary>
  26.         /// <returns>Returns True if the caller was the last to exit (based on the
  27.         /// number of TryEnter calls that were made)</returns>
  28.         public bool TryExit()
  29.         {
  30.             long result = Interlocked.Decrement(ref _entryAttempts);
  31.             if (result == 0)
  32.             {
  33.                 return true;
  34.             }
  35.             else if (result < 0)
  36.             {
  37.                 // Someone beat us to the punch, reset to the counter
  38.                 // to Zero to ensure we are able to enter again properly
  39.                 ExitClean();
  40.                 return false;
  41.             }
  42.             return false;
  43.         }
  44.         /// <summary>
  45.         /// Exits an Exclusive Section immediately regardless of how many calls
  46.         /// had previously been made to TryEnter and returns true if there
  47.         /// was no contention.
  48.         /// </summary>
  49.         /// <returns>Returns True if there was no more than one call to
  50.         /// TryEnter before the call to ExitClean was made</returns>
  51.         public bool ExitClean()
  52.         {
  53.             return (Interlocked.Exchange(ref _entryAttempts, 0) == 1);
  54.         }
  55.     \

The Final Result

Now I can change my code so that it uses an Exclusive Section to ensure the code only ever performs a single query at a time, and but the most recent text query entered by the user is always respected.

Code Snippet
  1.         private ExclusiveSection _searchSection = new ExclusiveSection();
  2.         private void SearchQueryTextBox_TextChanged(object sender, EventArgs e)
  3.         {
  4.             SearchWebAsync(SearchQueryTextBox.Text);
  5.         }
  6.         private void SearchWebAsync(string searchText)
  7.         {
  8.             if (_searchSection.TryEnter())
  9.             {
  10.                 WebClient client = new WebClient();
  11.                 Uri searchUrl = new Uri("http://www.google.com/search?q=&quot; + searchText);
  12.                 client.DownloadStringCompleted += WebClient_DownloadStringCompleted;
  13.                 client.DownloadStringAsync(searchUrl);
  14.             }
  15.         }
  16.         private void WebClient_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
  17.         {
  18.             if (!e.Cancelled && e.Error == null)
  19.             {
  20.                 SearchResultsTextBox.Text = e.Result;
  21.             }
  22.             if (!_searchSection.ExitClean())
  23.             {
  24.                 SearchWebAsync(SearchQueryTextBox.Text);
  25.             }
  26.         }

Interestingly the call to SearchWebAsync can now be made from any thread, and will remain thread safe, (as long as the WebClient_DownloadStringCompleted event performed an Invoke on the UI thread to access the Windows Forms controls). This could be useful if you wanted to add a timer thread that fires off a call to refresh the search results every 30 seconds or so.

This kind of threading class is extremely useful in Silverlight where only asynchronous calls to get data are allowed.

Hope you find the ExclusiveSection class as useful as I do!

http://cid-17124d03a9a052b0.skydrive.live.com/embedrowdetail.aspx/Public/ExclusiveSectionDemo.zip

Leave a comment