views:

750

answers:

2

In my WPF app I have a long running upload running, which raises progress events as it goes which updates a progress bar. The user also has a chance of cancelling the upload, or it might go wrong. These are all async events, so they need to be executed using Dispatcher.Invoke in order to update the UI.

So the code looks like this, ish:

void OnCancelButtonClicked(object sender, EventArgs e)
{
    upload.Cancel();
    _cancelled = true;
    view.Close();
    view.Dispose();
}

void OnProgressReceived(object sender, EventArgs<double> e)
{
    Dispatcher.Invoke(() => 
    {
        if (!cancelled)
            view.Progress = e.Value;
    }
}

Supposing that setting view.Progress on a disposed view is going to throw an error, is this code thread safe? i.e. if a user clicks cancel while the progress is updating, he/she will have to wait until the progress has been updated, and if the progress is updated during execution of OnCancelButtonClicked, the Dispatcher.Invoke call will cause the view.Progress update to be queued til after _cancelled is set, so I won't get a problem there.

Or do I need a lock to be safe, a la:

object myLock = new object();

void OnCancelButtonClicked(object sender, EventArgs e)
{
    lock(myLock)
    {
        upload.Cancel();
        _cancelled = true;
        view.Close();
        view.Dispose();
    }
}

void OnProgressReceived(object sender, EventArgs<double> e)
{
    Dispatcher.Invoke(() => 
    {
        lock(myLock)
        {
            if (!cancelled)
                view.Progress = e.Value;
        }
    }
}
A: 

This is an interesting question. Items executed in the dispatcher are queued and execute on the same thread as UI interaction. Here's the best article on this subject: http://msdn.microsoft.com/en-us/library/ms741870.aspx

If I were to venture a guess, I would say that Dispatcher.Invoke(Action) probably queues an atomic work item, so it's likely going to be ok, however I'm not sure if it wraps up your UI event handler in an atomic action item for instance:

//Are these bits atomic?  Not sure.
upload.Cancel();
_cancelled = true;

For safety's sake I would personally lock, but your question warrants more research. Might need a dive in reflector to figure out for sure.

As an aside, I'd probably optimize your lock a bit.

Dispatcher.Invoke(() => 
{
     if (!cancelled)
     {
          lock(myLock)
          {
               if(!cancelled)
                    view.Progress = e.Value;
          }
     }
}

But that's probably overkill :)

Anderson Imes
+2  A: 

you don't have to add a lock, Dispatcher.Invoke and BeginInvoke requests will not run in the middle of other code (that's the whole point of them).

Just two things to consider:

  1. BeginInvoke may be more appropriate in this case, Invoke will queue the request and then block the calling thread until the UI thread becomes idle and finishes executing the code, BeginInvoke will only queue the request without blocking.
  2. Some operations, especially operations that open windows (including message boxes) or do inter-process communication may allow the queued dispatcher operations to run.

EDIT: first, I don't have citations because the MSDN pages on the subject are unfortunately very low on details - but I have written test programs to check the behavior of BeginInvoke and everything I write here is the result of those tests.

now, to expand on the second point we first need to understand what the dispatcher does, obviously this is a very simplified explanation.

Any Windows UI work by processing messages, for example when the user moves the mouse over a window the system will send that window a WM_MOUSEMOVE message.

The system send the message by adding it a queue, each thread may have a queue, all windows created by the same thread share the same queue.

In the heart of every Windows program there's a loop called "message loop" or "message pump", this loop reads the next message from the queue and calls the appropriate window's code to process that message.

In WPF this loop and all the related processing handled by the Dispatcher.

An application can either be in the message loop waiting for the next message or it could be doing something, that is why when you have a long calculation all the thread's windows become unresponsive - the thread is busy working and doesn't return to the message loop to process the next message.

Dispatcher.Invoke and BeginInvoke work by queuing the requested operation and executing it the next time the thread returns to the message loop.

That is why Dispather.(Begin)Invoke can't "inject" code in the middle of your method, you won't get back to the message loop until your method returns.

BUT

Any code can run a message loop, when you call anything that runs a message loop the Dispatcher will be called and can run the (Begin)Invoke operations.

What kinds of code has a message loop?

  1. Anything that has a GUI or that excepts user input, for example dialog boxes, message boxes, drag&drop etc. - if those didn't have a message loop than the app would have been unresponsive and unable to handle user input.
  2. Inter-process communication that uses windows messages behind the scenes (most inter-process communication methods, including COM, use them).
  3. Anything else that takes a long time and doesn't freeze the system (the fast that the system isn't frozen is proof it's processing messages).

So, to summerize:

  • the Dispatcher can't just drop code into your thread, it can only execute code when the application is in the "message loop".
  • Any code you write doesn't have message loops unless you explicitly wrote them.
  • Most UI code doesn't have it's own message loop, for example if you call Window.Show and then do some long calculation the window will only appear after the calculation is finished and the method returns (and the app returns to the message loop and processes all the messages required to open and draw a window).
  • But any code that interacts with the user before it returns (MessageBox.Show, Window.ShowDialog) has to have a message loop.
  • Some communication code (network and inter-process) uses message loops, some doesn't, depending on the specific implementation you are using.
Nir
Could you expand a little on your second point please - what is the dangerous scenario?
mcintyre321
Citations would be nice, too.
Anderson Imes
Ok, I've added a short (only about 6 times as long as the original answer) explanation how and when Dispatcher.(Begin)Invoke works.
Nir
thankyou very much!
mcintyre321