Architecting WP7 - Part 9 of 10: Threading


Windows Phone 7 Architecture

In this ninth part of my series on architecting Windows Phone 7 (WP7) applications, i'll discuss threading on the phone. If you've missed the past parts of the series, you you can visit them here:

In the big picture, the applications you build for the Windows Phone 7 needs to super responsive. But the hardware is a phone so you have concrete limitations to what can and should be done on the phone. In the big picture, its usually better to shoot for perceived performance than try to overpower the CPU and GPU. So a judicious use of threading is important.

My opinion about threading has been the same for a while (pretty much since .NET 2.0 shipped).  If you are writing the line of code "new Thread()", you're doing it wrong. The framework (even the one in WP7) has a number of facilities to help you get the most of threading without managing your own threads. This is especially true on the phone as you are working with fewer CPU resources.  Instead of having to figure this out yourself, you should be using facilities like the ThreadPool or the BackgroundWorker. Let's talk about these two facilities and where it makes sense to use them.

Which One?

The two facilities that I tend to lean on are the ThreadPool and the BackgroundWorker. They have different use-cases. Sometimes you have tasks that you want process on a background thread so that the rendering or availablility (e.g. responsiveness) of your application isn't affected. That's where the BackgroundWorker class is really useful. The other case is where you want a number of things to happen eventually and don't want to overwhelm the CPU. That's where the ThreadPool can help.

BackgroundWorker

The BackgroundWorker is class is made precisely to be able to run code on a background thread while leaving the UI thread available. Even with WP7's rendering thread, this becomes useful in not starving the UI thread (so users can use the UI, not just rendering). The BackgroundWorker supports a number of features including:

  • Simply run code on a background thread
  • Graceful cancellation
  • Reporting Progress

In the typical case, you would create a BackgroundWorker object at the page or application level like so:

public partial class MainPage : PhoneApplicationPage
{
  BackgroundWorker worker = new BackgroundWorker();

Early in the lifecycle, you will set up the background worker by specifying what features you want:

// Set the features
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;

This allows the BackgroundWorker to be prepared for the certain features. You can then handle events that will be used during the lifecycle of the BackgroundWorker. The first is the DoWork event. This event is specially used as the place you put code that will happen on the background thread:

worker.DoWork += new DoWorkEventHandler(worker_DoWork);

...

void worker_DoWork(object sender, DoWorkEventArgs e)
{
  for (var x = 0; x < 100; ++x)
  {
    // Do Something
    Thread.Sleep(1000);
  }
}

To start the background thread, you'd call the worker's RunWorkerAsync method to start the background thread process:

// Launch the worker 
worker.RunWorkerAsync();

Another important event is the RunWorkerCompleted event. This event is fired when the background work is complete...whether it completes successfully or not. This is a critical event as if an exception is thrown in the DoWork event handler, there isn't anywhere to handle it (its not on the UI thread afterall), so you are passed the exception in the RunWorkerCompleted event:

worker.RunWorkerCompleted 
  += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);

...

void worker_RunWorkerCompleted(object sender, 
                               RunWorkerCompletedEventArgs e)
{
  if (e.Error != null)
  {
    // Happens on the UI thread so its ok
    MessageBox.Show("Error occurred...");
  }
}

While I don't have time to show it, you can also post information to the UI thread using progress notification as well as supporting cancellation with the BackgroundWorker.  If you have a task (or are waiting for a task to complete) that has to happen in the background, this is the class that will do the heavy lifting for you.

ThreadPool

In constrast to the BackgroundWorker, the ThreadPool is about having a set of background threads that are available for discrete tasks.  These tasks don't have to be small, but usually are of a fixed sized.  This was precisely the problem I had in my GooNews application. I had an unknown number of feeds and wanted them to load but knowing that trying to open a dozen network connections would leave me managing the connections or dealing with timeouts.  So instead I used the ThreadPool.

The ThreadPool can create a fixed number of threads that it can re-use to accomplish tasks. To use the ThreadPool you just throw your task in a queue and as threads are available, it executes the task. The ThreadPool class normally has four threads per CPU core, but on the Windows Phone 7 it has two threads per CPU (which means usually two).   What may not be obvious in this design, is that you can just queue up your requests and the ThreadPool will use as many threads as is appropriate for the hardware. You don't have to think about how many concurrent threads are a good idea.

To use the ThreadPool i'll show an example from GooNews. Each feed in GooNews has a method that is used to load the feed (called LoadFeedAsync):

public void LoadFeedAsync()
{
  if (!NetworkInterface.GetIsNetworkAvailable())
  {
    ErrorMessage = "No Network is Available. Try again later.";
  }
  else
  {
    FeedItems.Clear();
    ThreadPool.QueueUserWorkItem(new WaitCallback(DoLoadFeed));
  }
}

By calling QueueUserWorkItem, I am telling the ThreadPool to load this feed once a thread is available.  The DoLoadFeed method is the call that actually does the loading of the feed:

void DoLoadFeed(object ignore)
{
  IsBusy = true;
  WebClient client = new WebClient();

  // Note, that all of this happens on the UI thread...
  client.DownloadStringCompleted += (s, a) =>
  {
    try
    {
      if (a.Error == null)
      {
        var doc = XDocument.Parse(a.Result);
        XmlReader rdr = doc.CreateReader();
        var rssReader = new Rss20FeedFormatter();
        if (rssReader.CanRead(rdr))
        {
          rssReader.ReadFrom(rdr);
          List<FeedItem> newItems = new List<FeedItem>();
          foreach (var i in rssReader.Feed.Items)
          {
            newItems.Add(new FeedItem(i));
          }
          FeedItems.Clear();
          newItems.Foreach(i => Feeds.Add(i));
          RaiseLoadComplete();
        }
      }
      else
      {
        FeedItems.Clear();
        ErrorMessage = "Failed to load News Items";
      }
    }
    catch
    {
      FeedItems.Clear();
      ErrorMessage = "Failed to load News Items";
    }
    IsBusy = false;
  };
  client.DownloadStringAsync(new Uri(Url));
}

I've gotten a lot of kudo's for loading being fast, but the real trick here is that I am using off the shelf components (e.g. LINQ to XML, WCF's Syndication stack) and the ThreadPool. What you likely don't see is that as you're reading the stories on the first page, I continue to load the other pages.

What about the RX Framework?

The RX Framework is a great set of code for handling code like the ThreadPool, but I find that it requires me to centralize code that I don't want to centralize so I don't use it as much as I thought I would. Its not a bad solution, but the important story here is that you want to both move extraneous processing off the UI thread to stay responsive, but also not overwhelm the limited resources on a phone with too much background work. Using the ThreadPool and BackgroundWorker can help you do this.

<ShamelessPlug>If you like these articles, remember I am teaching Silverlight for the Windows Phone 7 on January 24th-26th, 2011 in Atlanta, GA!</ShamelessPlug>

 




Application Name WilderBlog Environment Name Production
Application Ver 2.0.0.0 Runtime Framework .NETCoreApp,Version=v2.0
App Path D:\home\site\wwwroot\ Runtime Version .NET Core 4.6.26020.03
Operating System Microsoft Windows 10.0.14393 Runtime Arch X86