Reputation: 2780
I have a WPF program with a button which creates and displays some data which is databound to a grid. The process of creating the data is very slow and CPU bound hence I offload it to a task. I want to display the first chunk of data as soon as its ready, then display the second chunk.
Here are 3 implementations which all work and keep the UI responsive.
await Dispatcher.InvokeAsync, Dispatcher.Invoke and Dispatcher.Invoke (inside the Task.Run). Which of these is going to avoid blocking a thread on the threadpool that could otherwise be doing work, and which is the least likely to result in a deadlock if someone had blocked the UI thread elsewhere in the program?
public ObservableCollection<BigObject> DataBoundList {get;set;}
public ObservableCollection<BigObject> DataBoundList2 {get;set;}
//Click handler from WPF UI button
public async void ClickHandlerCommand()
{
List<BigObject> items1 = null;
List<BigObject> items2 = null;
//On UI Thread
await Task.Run(() =>
{
//On thread X from threadpool
items1 = SlowCPUBoundMethod1();
}).ConfigureAwait(false);
Dispatcher.Invoke(() =>
{
//On UI Thread
DataBoundList = new ObservableCollection<BigObject>(items1);
RaisePropertyChanged(nameof(DataBoundList));
});
//On thread X from threadpool
await Task.Run(() =>
{
//On thread Y from threadpool
items2 = SlowCPUBoundMethod2();
}).ConfigureAwait(false);
//On thread Y from threadpool
Dispatcher.Invoke(() =>
{
//On UI Thread
DataBoundList2 = new ObservableCollection<BigObject>(items2);
RaisePropertyChanged(nameof(DataBoundList2));
});
//On thread Y from threadpool
//5x context switches
}
The implementation above puts the dispatcher call outside the Task.Run. This will likely cause two threads to be spun up. If another thread someone in the program had blocked the UI thread then I think the Dispatcher.Invoke call would possibly deadlock?
public async void ClickHandlerCommand2()
{
List<BigObject> items = null;
List<BigObject> items2 = null;
//On UI Thread
await Task.Run(() =>
{
//On thread X from threadpool
items1 = SlowCPUBoundMethod1();
Dispatcher.Invoke(() =>
{
//On UI thread
DataBoundList = new ObservableCollection<BigObject>(items1);
RaisePropertyChanged(nameof(DataBoundList));
});
//On thread X from threadpool
items2 = SlowCPUBoundMethod2();
Dispatcher.Invoke(() =>
{
//On UI thread
DataBoundList2 = new ObservableCollection<BigObject>(items2);
RaisePropertyChanged(nameof(DataBoundList2));
});
//On thread X from threadpool
}).ConfigureAwait(false);
//On thread X from threadpool
//5x context switches
}
The implementation above will have a single thread, however if another thread someone in the program had blocked the UI thread then I think the Dispatcher.Invoke call would possibly deadlock?
public async void ClickHandlerCommand3()
{
List<BigObject> items1 = null;
List<BigObject> items2 = null;
//On UI Thread
await Task.Run(() =>
{
//On thread X from threadpool
items1 = SlowCPUBoundMethod1();
}).ConfigureAwait(false);
//On thread X from threadpool
await Dispatcher.InvokeAsync(() =>
{
//On UI Thread
DataBoundList = new ObservableCollection<BigObject>(items1);
RaisePropertyChanged(nameof(DataBoundList));
});
//On thread X from threadpool
items2 = SlowCPUBoundMethod2();
await Dispatcher.InvokeAsync(() =>
{
//On UI Thread
DataBoundList2 = new ObservableCollection<BigObject>(items2);
RaisePropertyChanged(nameof(DataBoundList2));
});
//On thread X from threadpool
//5x context switches
}
This should result in only 1 task being spun up and I believe reduce the risk of a deadlock if someone somewhere else has blocked the UI thread. I think this is the best implementation?
Can someone categorically say which is the correct implementation? I believe the third example using await Dispatcher.InvokeAsync is the correct one but I'm not completely sure.
Upvotes: 2
Views: 5330
Reputation: 43846
This is not an answer to the question asked, which is about the difference between Dispatcher.Invoke
and Dispatcher.InvokeAsync
. I would like to share my personal preference between these two methods, which is to use neither. They are both ugly, cumbersome, and for the most part redundant. The Task.Run
is sufficient for offloading work to the ThreadPool
, and then awaiting the created Task<TResult>
is enough for grabbing the result of the computation, and using it on the UI thread:
public async void ClickHandlerCommand()
{
var items = await Task.Run(() => SlowCPUBoundMethod1());
DataBoundList = new ObservableCollection<BigObject>(items1);
RaisePropertyChanged(nameof(DataBoundList));
var items2 = await Task.Run(() => SlowCPUBoundMethod2());
DataBoundList2 = new ObservableCollection<BigObject>(items2);
RaisePropertyChanged(nameof(DataBoundList2));
}
In case more granular communication is needed between the UI and the background thread, one or more IProgress<T>
objects can be used for establishing this communication. The background thread is passed an IProgress<T>
object and uses it to report progress in an abstract way, and the UI thread receives these progress notifications and uses them for updating the UI. An example of using the IProgress<T>
interface can be found here. This is a good reading too: Async in 4.5: Enabling Progress and Cancellation in Async APIs.
Upvotes: 3
Reputation: 334
Both Dispatcher.Invoke, and InvokeAsync execute the delegate on the dispatcher's thread. The former does this synchronously, and will block the calling thread until the delegate finishes execution; the latter doesn't block the calling thread.
Both methods enque the delegate somewhere in the dispatcher's processing queue, based on the DispatcherPriority param (unless you use send priority, then Dispatcher.Invoke may bypass the queue and invoke the delegate immediately). It follows then, that the lower the priority, the longer the calling thread may be blocked while waiting for it to complete (if you use Dispatcher.Invoke).
The third approach, Task.Run(() => Dispatcher.Invoke()), doesn't block the original calling thread, but it does block the thread on which the task is running (presumably a thread pool thread).
Dispatcher.InvokeAsync is the best approach for your use-case, it was designed for this exact purpose.
Upvotes: 5