Search code examples
c#wpftasksuspendcancellation

Task cancellation suspends UI


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");
    }
}

Solution

  • 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.