Having an async Task yield to the Windows Forms SynchronizationContext

There are issues with calling Task.Yield in Windows Forms, as explained by Stephen Toub on this forum post. Interestingly, I have the opposite problem to the guy complaining there. I have lots of async code, much of which frequently switches back to the UI thread, but while executing some long-running code on the ThreadPool, my application freezes because the UI thread isn’t getting enough CPU time.

Again, the source, for this and a whole bunch of other stuff, is here: RomyView.zip
Incidentally, the code in this article was only just added to the solution last night, and the updated zip file on my SkyDrive was only uploaded today.

This happens in my Browser form, while it populates thumbnails. It’s fine if I browse a directory that contains only a few, or a few hundred files for which I will display thumbnails, but as soon as I browse a directory that displays thousands of thumbnails, everything falls apart. I won’t go into too much detail about what’s going on behind the scenes, but I need to give the overall flow here…

When I browse a directory, if the thumbnails are not already cached, they are retrieved using the IShellItemImageFactory Windows shell interface (for most thumbnails – that is for images and videos); while thumbnails already cached are retrieved from my custom cache. The Thumbnail controls themselves handle this asynchronously, and they are added in batches to an instance of a custom FlowLayoutPanel class.

The user experience is very different for thumbnails already cached, because they are loaded almost instantly, but that is not my focus here. My thumbnail controls draw placeholders if their images are not cached.

But while the thumbnail batches are added, the UI needs to remain responsive, so that the user can scroll, and so that the various drawing code for hot-tracked and selected thumbnails runs properly in response to the user moving the cursor. (Multiple selection is supported, and there is no reason for the user not to delete multiple of the files already displayed while more are being added.) Additionally, the user needs to be able to change the current directory. In that event, all pending thumbnail image loading Tasks need to be cancelled, and the thumbnail controls must be removed from the panel, and placed into an ObjectPool for reuse. But if the UI is frozen, none of this happens!

Aside: Testing this gets complicated, because Windows itself caches thumbnails, which means that my application will behave differently when browsing the same directory more than once, even after clearing my own cache. To work around this, I use cleanmgr.exe to clear the Windows cache periodically. It’s possible to create “sage sets” to run via cleanmgr. For example, I ran cleanmgr /sageset:2 and set it up just to clear thumbnails, then whenever I want to clear the Windows thumbnail cache, I just run cleanmgr /sagerun:2.

My solution

There are two parts to my solution. I’ll write about the second part first, although I’m not certain that the order is important.

Stephen Toub provided a possible solution to the person on the forum mentioned at the top of this post, in the form of a custom awaiter. This works very well, but is not quite enough for me. Here is the awaiter:

  1. public struct IdleAwaiter : INotifyCompletion
  2. {
  3.     private static Queue<Action> m_actions = new Queue<Action>();
  4.  
  5.     static IdleAwaiter()
  6.     {
  7.         Application.Idle += delegate
  8.         {
  9.             if (m_actions.Count > 0)
  10.             {
  11.                 SynchronizationContext.Current.Post(s => ((Action)s)(), m_actions.Dequeue());
  12.             }
  13.         };
  14.     }
  15.  
  16.     public IdleAwaiter GetAwaiter() { return this; }
  17.  
  18.     public bool IsCompleted { get { return false; } }
  19.  
  20.     public void GetResult() { }
  21.  
  22.     public void OnCompleted(Action continuation)
  23.     {
  24.         m_actions.Enqueue(continuation);
  25.     }
  26. }

 

Unfortunately Task.Yield just targets whatever the captured SynchronizationContext is, but once things get complex, you may not know what the current SynchronizationContext may be, or even if there is one. Thus I wrote a simple extension method on Task, to target a captured SynchronizationContext .

First of all, I added this to my base form class, so that all forms have a reference to a TaskScheduler that targets the Windows Forms SynchronizationContext.

  1. private TaskScheduler uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
  2.  
  3. [Browsable(false), CLSCompliant(false)]
  4. public TaskScheduler UiTaskScheduler
  5. {
  6.     get { return uiTaskScheduler; }
  7. }

 

The extension method is just this:

  1. public static Task YieldTo(this Task task, TaskScheduler scheduler)
  2. {
  3.     return task.ContinueWith(antecedent =>
  4.     {
  5.         Task.Yield();
  6.         return antecedent.ConfigureAwait(false);
  7.     }, scheduler);
  8. }

 

Now, in my Browser form, in the loop whenever I am about to add a batch of thumbnails (and batch below is a Task that returns a collection of thumbnails to add), I simply call this:

  1. await batch.YieldTo(UiTaskScheduler);
  2.  
  3. await default(IdleAwaiter);

 

And now my UI remains responsive!

Advertisements

About Jerome

I am a senior C# developer in Johannesburg, South Africa. I am also a recovering addict, who spent nearly eight years using methamphetamine. I write on my recovery blog about my lessons learned and sometimes give advice to others who have made similar mistakes, often from my viewpoint as an atheist, and I also write some C# programming articles on my programming blog.
This entry was posted in Programming and tagged , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s