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:

 

 
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.

 

 

 

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.

 

This entry was posted in Uncategorized. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Tom

    Looks great but the link seems to be broken.

  • doc_net

    Thank you very much for this article – I’ve been looking for a good way to implement this for a while.

    A word of warning for anyone using it in a WPF application. Make sure you use a (System.Windows.Threading) DispatcherTimer otherwise you will not be able to manipulate anything on the UI thread in your RunWorkerCompleted event.

    Took a while to work that one out myself.

  • http://rerew@rsdr.fdfd desadda

    Earlier only link got broken now to that link is gayab….

  • Scott

    The link to sample project is broken again. Thanks!

  • Brian Elgaard

    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.

  • Brian Elgaard

    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.

  • Eric

    What’s the definition for ‘TArgument’??

  • Duncan

    Thank you SO much.

    Been struggling with this for ages.

  • http://www.NDepend.com Patrick Smacchia

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

  • http://huseyint.com/ Hüseyin Tüfekçilerli

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