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.