I am little confuced by the followed situation. If I call SleepBeforeInvoke
method, application is suspended on the _task.Wait();
string. But if I call SleepAfterInvoke
method, application works fine and control will reach catch
clause. Calling the BeginInvoke
method works fine as well.
Could anyone explain with maximum details what's the difference between these three methods usage? Why application is suspended if I use SleepBeforeInvoke
method, and why it is not if I use SleepAfterInvoke
and BeginInvoke
methods? Thanks.
Win 7, .Net 4.0
xaml:
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Name="_textBlock"
Text="MainWindow"></TextBlock>
<Button Grid.Row="1"
Click="ButtonBase_OnClick"></Button>
</Grid>
.cs:
public partial class MainWindow : Window
{
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
private Task _task;
/// <summary>
/// Application wiil be suspended on string _task.Wait();
/// </summary>
private void SleepBeforeInvoke()
{
for (Int32 count = 0; count < 50; count++)
{
if (_cts.Token.IsCancellationRequested)
_cts.Token.ThrowIfCancellationRequested();
Thread.Sleep(500);
Application.Current.Dispatcher.Invoke(new Action(() => { }));
}
}
/// <summary>
/// Works fine, control will reach the catch
/// </summary>
private void SleepAfterInvoke()
{
for (Int32 count = 0; count < 50; count++)
{
if (_cts.Token.IsCancellationRequested)
_cts.Token.ThrowIfCancellationRequested();
Application.Current.Dispatcher.Invoke(new Action(() => { }));
Thread.Sleep(500);
}
}
/// <summary>
/// Works fine, control will reach the catch
/// </summary>
private void BeginInvoke()
{
for (Int32 count = 0; count < 50; count++)
{
if (_cts.Token.IsCancellationRequested)
_cts.Token.ThrowIfCancellationRequested();
Thread.Sleep(500);
Application.Current.Dispatcher.BeginInvoke(new Action(() => { }));
}
}
public MainWindow()
{
InitializeComponent();
_task = Task.Factory.StartNew(SleepBeforeInvoke, _cts.Token, TaskCreationOptions.None, TaskScheduler.Default);
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
try
{
_cts.Cancel();
_task.Wait();
}
catch (AggregateException)
{
}
Debug.WriteLine("Task has been cancelled");
}
}
Both SleepBeforeInvoke
and SleepAfterInvoke
have a potential deadlock in them due to the Dispatcher.Invoke
call - it's just that you're that much more likely to hit it in SleepBeforeInvoke
because you're creating an artificial 500ms delay where the problem will occur, as opposed to a negligible (probably nanoseconds) window in the other case.
The issue is due to the blocking nature of Dispatcher.Invoke
and Task.Wait
. Here's what your flow for SleepBeforeInvoke
roughly looks like:
The app starts and the task is wired up.
The task runs on a thread pool thread, but periodically blocks on a synchronous call marshalled to your UI (dispatcher) synchronization context. The task has to wait for this call to complete before it can proceed to the next loop iteration.
When you press the button, cancellation will be requested. It will most likely happen while the task is executing
Thread.Sleep
. Your UI thread will then block waiting for the task to finish (_task.Wait
), which will never occur, because right after your task finishes sleeping it won't check whether it's been cancelled and will try to make a synchronous dispatcher call (on the UI thread, which is already busy due to_task.Wait
), and ultimately deadlock.
You could (sort of) fix this by having another _cts.Token.ThrowIfCancellationRequested();
after the sleep.
The reason the problem is not observed in the SleepAfterInvoke
example is timing: your CancellationToken
is always checked right before the synchronous dispatcher call, thus the likelihood that the call to _cts.Cancel
will occur between the check and the dispatcher call is negligible, as the two are very close together.
Your BeginInvoke
example does not exibit the above behaviour at all because you are removing the very thing which causes the deadlock - blocking call. Dispatcher.BeginInvoke
is non-blocking - it just "schedules" an invoke on the dispatcher sometime in the future and returns immediately without waiting for the invocation to complete, thus allowing the thread pool task to move on to the next loop iteration, and hit ThrowIfCancellationRequested
.
Just for fun: I suggest you put something like Debug.Print
inside the delegate that you're passing to Dispatcher.BeginInvoke
, and another one right after _task.Wait
. You will notice that they do not execute in the order that you expect due to the fact that _task.Wait
blocks the UI thread meaning that the delegate passed to Dispatcher.BeginInvoke
after the cancellation has been requested doesn't get to execute until your button handler finishes running.