Patrick Smacchia [MVP C#]

Sponsors

The Lounge

Advertisement

Images in this post missing? We recently lost them in a site migration. We're working to restore these as you read this. Should you need an image in an emergency, please contact us at imagehelp@codebetter.com
BackgroundWorker Closure and Overridable Task

 Clearly, the class System.ComponentModel.BackgroundWorker is a good friend for all Windows Form programmers. Basically, it lets developers delegate a computation intensive task to a non-UI thread in order to get a responsive UI. The BackgroundWorker class lets specify a callback procedure through the event RunWorkerCompleted. The real benefit of the BackgroundWorker class is that this callback procedure is ran by the UI thread once the task done.

 
The BackgroundWorker class also addresses 2 useful scenario: task cancellation and progression report callback on the UI thread.

 

 

The Overridable Task Scenario 

I found an important scenario that is not addressed by the BackgroundWorker class: overridable task. What I want is to trigger a task and be able to trigger again the same task without waiting for the first occurrence to complete. The first task must be cancelled before the second one being run since a BackgroundWorker object cannot execute 2 tasks at the same time.

 

I found this scenario useful because it lets refresh a sophisticated UI in real-time on intensive user input, without degrading UI responsiveness. Typically, this happens when you’re implementing some kind of intelligence on mouse move when a non-trivial computation is triggered each time the MouseMove event is raised (typically several dozen times a second). This happens also on some text typing facility such as intellisense, where the user can type several characters a second and the computation might take a second or even more to be computed. In both cases, the UI must remain responsive and the computation has to be re-trigged, each time a new character is inputted or each time the MouseMove event provides new mouse coordinates.

 

Here is a concrete use of the overridable task pattern in the NDepend UI. When editing a CQL query, both the list of code elements matched by the query (the Query Result panel), and the treemap where matched code elements are painted in blue (the Metric panel) gets updated in real time. The screenshot below shows that the intellisense lets the user modify the threshold on the query: SELECT METHODS WHERE NbParameters > {Threshold}. On large code base with hundreds of thousands of methods, the UI must remain responsive when the user moves the cursor of the intellisense to update the threshold. As a consequence, we needed the overridable task pattern to update matched methods in the Query Result and the Metric panel.

 

 

 

Implementation of the Overridable Task scenario with BackgroundWorker  timer and closure

At first glance, the BackgroundWorker comes with 2 convenient members: CancelAsync() and IsBusy(). However, the overridable task can’t be implemented naively by just calling CancelAsync() and then calling IsBusy() in an infinite loop containing a Thread.Sleep( a few microseconds here ).

  • First, this might kill responsiveness since the UI thread is blocked while waiting for the first task to be cancelled.
  • Second, calling Thread.Sleep(...) in an infinite loop is a typical anti-pattern that wastes thread, a proper timer must always be used in such circumstances.
  • And third, even worth, this just doesn’t work: The IsBusy() method will never return false! I am not sure about the implementation of BackgroundWorker but it seems that the internal isBusy state must be reset internally from the UI thread. Thus, you need to release the UI thread and then re-enter one of your procedure before observing IsBusy() returning false.

 

The only right approach here is to create a System.Window.Forms.Timer object triggered just after calling CancelAsync(). The timer will tick until the method IsBusy() returns false, and then it’ll be time to push the new task. So basically one needs to create a timer field + a timer callback procedure + a state field to store the input data of the new task to trigger. This sort of situation is well handled by closures. Here is the method BeginWork() code with a closure that avoids to create extra fields and extra procedures:

 

public void BeginWork(TArgument argument) { if (!m_BackgroundWorker.IsBusy) { m_BackgroundWorker.RunWorkerAsync(argument); return; } m_BackgroundWorker.CancelAsync(); System.Windows.Forms.Timer timerRetryUntilWorkerNotBusy = new System.Windows.Forms.Timer(); timerRetryUntilWorkerNotBusy.Tick += // Here is the closure: // m_BackgroundWorker, timerRetryUntilWorkerNotBusy, argument // gets all 3 captured. This avoids defining some extra fields // to store them across timer ticks. delegate { if (m_BackgroundWorker.IsBusy) { return; } timerRetryUntilWorkerNotBusy.Stop(); m_BackgroundWorker.RunWorkerAsync(argument); }; // Make sure to understand that this following code // gets executed before the closure's code. timerRetryUntilWorkerNotBusy.Interval = 1; timerRetryUntilWorkerNotBusy.Start(); return; }

 
Notice that I use here a C#2 anonymous method to implement the closure. A lambda expression can be used also, but in this particular case it is less concise since the 2 tick procedure parameters (sender and arg) would need to be specified.

 

A bug in the implementation

While this first implementation seems to me correct at first sight, it contains a subtle bug which effects appear only on certain condition. Imagine that the currently running task takes time to be cancelled. To make things concrete, I provide here a prototype where the task can be cancelled every 500ms only and where the task takes 3s to be computed. What happens if I trigger several time a second the task by clicking quickly the Go! Button? The following screenshot shows that actually, several timers gets created and they are competing to make their tasks run. And we can see that the task#7, the last one triggered, is actually not the one that ends up being executed thoroughly.

 

Correcting the bug

There is no way to fix this bug without adding at least one instance field. Indeed, the fact that a new task is waiting to be ran must be stored across each call to BeginWork(). This storage cannot be done with a captured state by a closure because it could then not be read from further calls to BeginWork(). Let’s add m_Argument field. If it is not null, it means that there is a task pending to be ran. Thus a second timer cannot be triggered, but the input argument to the task needs to be updated with the new input.

 

 

private TArgument m_Argument; public void BeginWork(TArgument argument) { // (m_Argument != null) // must be tested before m_BackgroundWorker.IsBusy in case a // remaining task is waited to be executed by the next timer tick. if (m_Argument != null) { m_Argument = argument; return; } if (!m_BackgroundWorker.IsBusy) { m_BackgroundWorker.RunWorkerAsync(argument); return; } m_Argument = argument; m_BackgroundWorker.CancelAsync(); System.Windows.Forms.Timer timerRetryUntilWorkerNotBusy = new System.Windows.Forms.Timer(); timerRetryUntilWorkerNotBusy.Tick += delegate { if (m_BackgroundWorker.IsBusy) { return; } timerRetryUntilWorkerNotBusy.Stop(); m_BackgroundWorker.RunWorkerAsync(m_Argument); m_Argument = null; }; timerRetryUntilWorkerNotBusy.Interval = 1; timerRetryUntilWorkerNotBusy.Start(); return; }

 

Nothing is obvious with asynchronous programming. Take the time to understand why  the scope (m_Argument != null)  comes before the scope (!m_BackGroundWorker.IsBusy). This lets handle the scenario where a background timer is still waiting for the current task to end up (because it has been cancelled)  and  between 2 ticks, a new task is posted.  Thus m_Argument is updated, and the newer task will be executed as soon as the timer can start it on the BackgroundWorker.

 


At first sight, accesses to m_Argument should be synchronized because the field can be written both by entering BeginWork(...) or by a timer tick. One elegant aspect is that these accesses are always done by the same thread, the UI thread. In other words the field m_Argument has an affinity with the UI thread. As a consequence, there is no need for synchronization.

 


Posted 06-10-2008 6:36 PM by Patrick Smacchia

[Advertisement]

Comments

Dew Drop – June 11, 2008 | Alvin Ashcraft's Morning Dew wrote Dew Drop – June 11, 2008 | Alvin Ashcraft's Morning Dew
on 06-11-2008 8:35 AM

Pingback from  Dew Drop – June 11, 2008 | Alvin Ashcraft's Morning Dew

Hüseyin Tüfekçilerli wrote re: BackgroundWorker Closure and Overridable Task
on 06-12-2008 3:42 AM

Great article! I will probably need to use this pattern in the future. Btw the link to sample project seems to be broken.

Patrick Smacchia wrote re: BackgroundWorker Closure and Overridable Task
on 06-12-2008 5:09 AM

Indeed, the link was broken, it is now working, thanks

Duncan wrote re: BackgroundWorker Closure and Overridable Task
on 06-24-2008 11:50 AM

Thank you SO much.

Been struggling with this for ages.

Eric wrote re: BackgroundWorker Closure and Overridable Task
on 02-03-2009 9:21 AM

What's the definition for 'TArgument'??

Brian Elgaard wrote re: BackgroundWorker Closure and Overridable Task
on 04-23-2009 3:35 AM

This is very useful!

Do you know if IsBusy() will return true before the RunWorkerCompleted call-back method has finished?

That would be unfortunate if the clean-up code here also needs to be run by a single thread at a time.

Brian Elgaard wrote re: BackgroundWorker Closure and Overridable Task
on 04-23-2009 5:26 AM

Well, as a partial answer to my own question I can at least conclude that IsBusy is false while RunWorkerCompleted is run.

This means that it might run (in the UI thread) while a worker thread is running.

To me, this means that I will have to not do any clean-up using shared resources in RunWorkerCompleted.

Scott wrote re: BackgroundWorker Closure and Overridable Task
on 06-05-2009 12:52 PM

The link to sample project is broken again. Thanks!

Add a Comment

(required)  
(optional)
(required)  
Remember Me?