2014-09-11 5 views
2

Я немного в замешательстве от сложившейся ситуации. Если я вызываю метод SleepBeforeInvoke, приложение приостанавливается на строку _task.Wait();. Но если я назову метод SleepAfterInvoke, приложение работает нормально, и управление достигнет catch. Вызов метода BeginInvoke также прекрасен.Отмена заданий отменяет UI

Может ли кто-нибудь объяснить с максимальными подробностями, в чем разница между этими тремя методами использования? Почему приложение приостановлено, если я использую метод SleepBeforeInvoke, и почему это не так, если я использую методы SleepAfterInvoke и BeginInvoke? Благодарю.

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

Задача async должна заменить вашу потребность в потоке. Если вы не знаете, что делаете, придерживайтесь Single Threaded Async. Также прочитайте блог Стивена Клири в асинхронном режиме. – Aron

+0

@Aron, .net 4.0 :(no async-wait – monstr

+0

Я бы предложил использовать Rx.Net в этом случае. Или обновить ... серьезно ... его стоит обновить ... хотя бы потому, что нарушена 'Task'. net 4.0, НЕ ИСПОЛЬЗУЙТЕ ЭТО. 'Task.ContinueWith' # $% * добавляет ваш' SynchronizationContext.Current' и в основном разбивает как WinForms, так и WPF. – Aron

ответ

3

Оба SleepBeforeInvoke и SleepAfterInvoke есть потенциальный тупик в них из-за Dispatcher.Invoke вызова - это просто, что вы гораздо чаще попадаете в SleepBeforeInvoke, потому что йо создаем искусственную задержку в 500 мс, когда проблема будет возникать, в отличие от незначительного (возможно, наносекундного) окна в другом случае.

Вопрос связан с блокирующим характером Dispatcher.Invoke и Task.Wait. Вот что ваш поток для SleepBeforeInvoke примерно выглядит так:

Приложение запускается, и задача подключена.

Задача выполняется в потоке пула потоков, но периодически блокируется при синхронном вызове, упорядоченном в контексте синхронизации пользовательского интерфейса (диспетчера). Задача должна дождаться завершения этого звонка, прежде чем он сможет перейти к следующей итерации цикла.

Когда вы нажмете кнопку, будет запрошена отмена. Скорее всего, это произойдет во время выполнения задачи Thread.Sleep. Затем ваш поток пользовательского интерфейса блокирует ожидание завершения задачи (_task.Wait), что никогда не произойдет, потому что сразу после завершения вашей задачи спать не будет проверяться, отменено ли оно и попытается выполнить синхронный диспетчерский вызов (в пользовательском интерфейсе нить, которая уже занята из-за _task.Wait) и, в конечном счете, тупик.

Вы можете (сорт) исправить это, имея следующий _cts.Token.ThrowIfCancellationRequested(); после сна.

Причины проблема не наблюдается в SleepAfterInvoke примере синхронизация: ваш CancellationToken всегда проверяются перед синхронной диспетчерской вызова, таким образом, вероятности того, что вызов _cts.Cancel будет происходить между чеком и диспетчерским вызовом пренебрежимо малым, так как они очень близки друг другу.

Ваш пример BeginInvoke вообще не подтверждает описанную выше ситуацию, поскольку вы удаляете ту самую вещь, которая вызывает блокировку блокировки.Dispatcher.BeginInvoke не блокирует - он просто «планирует» вызов в диспетчере когда-то в будущем и немедленно возвращается, не дожидаясь завершения вызова, тем самым позволяя заданию пула потоков перейти к следующей итерации цикла и нажать ThrowIfCancellationRequested.

Просто для удовольствия: Я предлагаю вам поставить что-то вроде Debug.Print внутри делегата, который вы передаете в Dispatcher.BeginInvoke, а еще один сразу после _task.Wait. Вы заметите, что они не выполняются в том порядке, в котором вы ожидаете, из-за того, что _task.Wait блокирует поток пользовательского интерфейса, что означает, что делегат, переданный в Dispatcher.BeginInvoke после того, как запрос был отменен, не может быть выполнен до тех пор, пока ваш обработчик кнопки не завершится.

+0

получил его, спасибо :-) – monstr

+0

@monstr, рад помочь. * Пожалуист. * –