2

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");
    }
}
monstr
  • 1,680
  • 1
  • 25
  • 44
  • Task async should replace your need for threading. Unless you know what you are doing, stick with Single Threaded Async. Also read Stephen Cleary's blog on async. – Aron Sep 11 '14 at 06:51
  • @Aron, .net 4.0 :( no async-await – monstr Sep 11 '14 at 06:59
  • I would suggest using Rx.Net in that case. Or upgrade...seriously...its worth upgrading...if only because `Task` is broken in .net 4.0, DO NOT USE IT. `Task.ContinueWith` #$%*s up your `SynchronizationContext.Current`, and basically breaks both WinForms and WPF. – Aron Sep 11 '14 at 07:00
  • @Aron, *broken* is a very strong word. You should back up your comment with some quality reading for those interested in the topic (such as myself). – Kirill Shlenskiy Sep 11 '14 at 07:23
  • @KirillShlenskiy [The SynchronizationContext.Current property is usually used to schedule work on the UI thread. Without it, .net will default to the ThreadPool, which will cause an STA exception on the ThreadPool and most likely exit from the program, instantly.](http://stackoverflow.com/questions/20953281/workaround-for-issue-in-net-4-0-where-synchronizationcontext-current-is-null) – Aron Sep 11 '14 at 07:26
  • @Aron, I have one project that works in 4.0 and doesn't in 4.5. and it is not a joke :( – monstr Sep 11 '14 at 07:38
  • @Aron, thanks, that's an interesting read. It prompted me to do some googling too and the issue appears to be more widespread than I expected. You learn something new every day. – Kirill Shlenskiy Sep 11 '14 at 07:40

1 Answers1

3

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.

Community
  • 1
  • 1
Kirill Shlenskiy
  • 9,367
  • 27
  • 39