WPF, TPL, шаблон производителя/потребителя — ошибка неправильного потока

Я новичок в TPL и WPf и столкнулся со следующей проблемой. Я пытаюсь загрузить сайт в бесконечном цикле (здесь только цикл for) и добавить его в очередь. Следующая задача берет его и показывает в текстовом блоке. Однако я, кажется, не понимаю правильный поток для пользовательского интерфейса, хотя я думаю, что правильно использую TaskScheduler.

Спасибо за любую помощь!

BlockingCollection<string> blockingCollection = new BlockingCollection<string>();
CancellationToken token = tokenSource.Token;
TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();

        Task task1 = new Task(
            (obj) =>
            {
                for (int i = 0; i < 10; i++)
                {
                    if (token.IsCancellationRequested)
                    {
                        TxtBlock2.Text = "Task cancel detected";
                        throw new OperationCanceledException(token);
                    }
                    else
                    {
                        string code = i.ToString() + "\t" + AsyncHttpReq.get_source_WebRequest(uri);
                        blockingCollection.Add(code);
                    }
                }
            }, TaskScheduler.Default);


        task1.ContinueWith(antecedents =>
        {
            TxtBlock2.Text = "Signalling production end";
            blockingCollection.CompleteAdding();
        }, uiScheduler);


        Task taskCP = new Task(
            (obj) =>
            {
                while (!blockingCollection.IsCompleted)
                {
                    string dlCode;
                    if (blockingCollection.TryTake(out dlCode))
                    {
     //the calling thread cannot access this object because a different thread owns it.
                        TxtBlock3.Text = dlCode;  
                    }
                }
            }, uiScheduler);


WindowsBase.dll!System.Windows.Threading.Dispatcher.VerifyAccess() + 0x4a bytes 
WindowsBase.dll!System.Windows.DependencyObject.SetValue(System.Windows.DependencyProperty dp, object value) + 0x19 bytes   
PresentationFramework.dll!System.Windows.Controls.TextBlock.Text.set(string value) + 0x24 bytes 

WpfRibbonApplication4.exe!WpfRibbonApplication4.MainWindow.Button1_Click.AnonymousMethod__4(object obj) Строка 83 + 0x16 байт C# mscorlib.dll!System.Threading.Tasks.Task.InnerInvoke() + 0x44 байт mscorlib.dll!System.Threading.Tasks.Task .Execute() + 0x43 байта mscorlib.dll!System.Threading.Tasks.Task.ExecutionContextCallback(object obj) + 0x27 байт
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executeContext, System Обратный вызов .Threading.ContextCallback, состояние объекта, bool ignoreSyncCtx) + 0xb0 байт
mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot) + 0x154 байт
mscorlib. dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) + 0x8b байт
mscorlib.dll!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() + 0x7 байт mscorlib.dll!System .Threading.ThreadPoolWorkQueue.Disp atch() + 0x147 байт
mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() + 0x2d байт
[переход из исходного состояния в управляемый]

   System.InvalidOperationException was unhandled by user code
  Message=The calling thread cannot access this object because a different thread owns it.
  Source=WindowsBase
  StackTrace:
       at System.Windows.Threading.Dispatcher.VerifyAccess()
       at System.Windows.DependencyObject.SetValue(DependencyProperty dp, Object value)
       at System.Windows.Controls.TextBlock.set_Text(String value)
       at WpfRibbonApplication4.MainWindow.<>c__DisplayClass5.<Button1_Click>b__3(Object o) in C:\ ... \WpfRibbonApplication4\WpfRibbonApplication4\MainWindow.xaml.cs:line 90
       at System.Threading.Tasks.Task.InnerInvoke()
       at System.Threading.Tasks.Task.Execute()
  InnerException: 

Большое спасибо за вашу помощь. У меня все еще есть два вопроса: я немного переписал свой код с помощью Task.Factory.StartNew. Однако моя Task2, похоже, вызывает проблемы. Там нет сообщения об ошибке. Похоже на тугую петлю. Я, конечно, не понял, почему? Не могли бы вы быть так любезны и снова указать мне правильное направление. Имейте в виду, что я занимаюсь C# примерно 6 месяцев, а TPL неделю, иначе я бы не стал вас снова спрашивать. Но с таким количеством опыта... Еще раз спасибо!

Код Брайанса:

var task1 = new Task( 
  (obj) => 

Зачем нужен obj?

private void Button1_Click(object sender, RoutedEventArgs e)
        {

TaskScheduler uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); BlockingCollection blockingCollection = new BlockingCollection(); CancellationTokenSource cts = новый CancellationTokenSource();

            CancellationToken token = cts.Token;

            Task task1 = Task.Factory.StartNew(
                () =>
                {
                    for (int i = 0; i < 10 ; i++)
                    {
                        token.ThrowIfCancellationRequested();
                        string code = i++.ToString() + "\t" + AsyncHttpReq.get_source_WebRequest(uriDE);
                        blockingCollection.Add(code);
                    }
                }, token, TaskCreationOptions.None, TaskScheduler.Default);

            task1.ContinueWith(
                (antecedents) =>
                {
                    if (token.IsCancellationRequested)
                    {
                        TxtBlock2.Text = "Task cancel detected"; 
                    }
                    else 
                    { 
                        TxtBlock2.Text = "Signalling production end"; 
                    }

                    blockingCollection.CompleteAdding();

                }, uiTaskScheduler);


            Task task2 = Task.Factory.StartNew(
                () =>
                {
                    while (!blockingCollection.IsCompleted)
                    {
                        string dlcode;

                        if (blockingCollection.TryTake(out dlcode))
                        {
                            TxtBlock3.Text = dlcode;
                        }
                    }

                }, token, TaskCreationOptions.None, uiTaskScheduler);

        }

person user774326    schedule 04.08.2011    source источник
comment
Не могли бы вы вставить полную трассировку стека? Кроме того, должен ли изначально запускаться метод, вызываемый из потока пользовательского интерфейса?   -  person Drew Marsh    schedule 04.08.2011
comment
Да, на самом деле они запускаются из потока пользовательского интерфейса. Все это объявлено в следующем событии: private void Button1_Click(object sender, RoutedEventArgs e) { } Я вставил стек выше.   -  person user774326    schedule 04.08.2011
comment
Извините, не могли бы вы вставить полную информацию об исключении (например, exception.ToString())? Это должно абсолютно работать. Вызов Dispatcher.[Begin]Invoke не требуется. Также у вас есть ошибка, из-за которой, если работа будет отменена, вы установите TextBlock2.Text в запланированной задаче без пользовательского интерфейса (задача1).   -  person Drew Marsh    schedule 04.08.2011
comment
Я добавил это. Я надеюсь, что это правильная трассировка стека. Спасибо!   -  person user774326    schedule 04.08.2011


Ответы (4)


Хорошо, на самом деле я только что снова посмотрел на ваш код, и проблема проста: вы создаете новые экземпляры Task вручную, используя перегрузку конструктора, которая принимает объект состояния. Нет перегрузки конструктора, которая принимает TaskScheduler.

Обычно люди используют Task.Factory.StartNew, поэтому я даже не заметил, что вы создавали Tasks вручную. Когда вы создаете экземпляры Task вручную, правильный способ указать планировщик для их запуска — использовать перегрузку Start, которая принимает экземпляр TaskScheduler. Итак, в вашем случае вам нужно будет сделать:

taskCP.Start(uiTaskScheduler);

ОБНОВЛЕНИЕ

Итак, теперь проблема с вашим обновленным кодом заключается в том, что вы фактически запланировали поток Task в пользовательском интерфейсе (диспетчер), который сидит там и читает в тесном цикле.

Теперь, когда код переработан, становится ясно, что вы не хотите планировать задачу 2 в потоке пользовательского интерфейса. Если вы хотите отправлять уведомления оттуда в пользовательский интерфейс, вы можете либо вызвать Dispatcher::BeginInvoke, как это предлагается в другом ответе, либо запустить новую задачу, используя uiTaskScheduler внутри цикла. Вызов Dispatcher::BeginInvoke будет иметь меньше накладных расходов и ИМХО будет более понятным кодом, поэтому я рекомендую просто сделать это.

person Drew Marsh    schedule 04.08.2011
comment
Спасибо. Я добавил еще один и переписал его с помощью Task.Factory.StartNew. - person user774326; 04.08.2011
comment
Хорошо, теперь я это понимаю. С моей стороны было скорее непонимание того, как сами задачи работают в связке с Диспетчером. Спасибо за вашу помощь в улучшении моего кода и за то, что показали мне, над чем мне нужно работать и практиковаться, чтобы стать лучше. - person user774326; 05.08.2011
comment
У меня было ощущение, что мы могли бы закончить здесь с самого начала, но ваш настоящий вопрос заключался в том, почему ваши задачи не выполнялись в потоке пользовательского интерфейса, и я действительно хотел добраться до сути этого, чтобы получить правильный ответ на ваш вопрос. Рад помочь! - person Drew Marsh; 05.08.2011
comment
Иногда бывает немного сложно задать точный вопрос без достаточных знаний и опыта. Некоторые вопросы и проблемы возникают в ходе беседы. У меня также была проблема с дженериками и Intellisense. Вот почему я также благодарен за ваше терпение и непонимание! Еще раз, большое спасибо! - person user774326; 05.08.2011

Вы можете использовать Dispatcher для доступа к потоку пользовательского интерфейса, например:

Dispatcher.BeginInvoke(new Action(()=>{TxtBlock2.Text = "Signalling production end";}));

Поскольку задача выполняется в другом потоке, отличном от потока пользовательского интерфейса, Dispatcher дает вам изменения для доступа к потоку, в котором находится ваш пользовательский интерфейс. MSDN дает хорошее объяснение по этому поводу, просмотрите часть примечаний по адресу: http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.aspx

Надеюсь, поможет.

person Howard    schedule 04.08.2011
comment
Код диспетчера работал как шарм :). Спасибо, я до сих пор не понимаю, почему мой код TPL не работает. - person user774326; 04.08.2011

Есть пара проблем с вашим кодом.

  • Нет перегрузки Task ctor, который принимает TaskScheduler. Что вы на самом деле сделали, так это передали параметр TaskScheduler в параметр state, который затем выбирается в вашей переменной obj в лямбда-выражении.
  • Из-за пункта выше taskCP фактически работает в планировщике по умолчанию, а не uiScheduler.
  • Из-за пункта выше taskCP пытается получить доступ к элементу пользовательского интерфейса из потока, отличного от пользовательского интерфейса, путем изменения TxtBlock3.
  • Точно так же task1 пытается сделать то же самое, изменяя TxtBlock2.

Вот как я бы реорганизовал код.

var queue = new BlockingCollection<string>();
var cts = new CancellationTokenSource();
TaskScheduler ui = TaskScheduler.FromCurrentSynchronizationContext();

var task1 = new Task(
  () =>
  {
    for (int i = 0; i < 10; i++)
    {
      token.ThrowIfCancellationRequested();
      string code = i.ToString() + "\t" + AsyncHttpReq.get_source_WebRequest(uri);
      queue.Add(code);
    }
  });

  task1.ContinueWith(
    antecedents =>
    {
      if (token.IsCancellationRequested)
      {
        TxtBlock2.Text = "Task cancel detected";
      }
      else
      {
        TxtBlock2.Text = "Signalling production end";
      }
      queue.CompleteAdding();
    }, ui);


  var taskCP = new Task(
    () =>
    {
      while (!queue.IsCompleted)
      {
        string dlCode;
        if (queue.TryTake(out dlCode))
        {
          Dispatcher.Invoke(() =>
          {
            TxtBlock3.Text = dlCode; 
          }
        }
      }
    });

  task1.Start();
  taskCP.Start();

Обратите внимание, что ContinueWith может принимать TaskScheduler, и это именно то, что я сделал выше. У меня также есть taskCP, работающий в планировщике по умолчанию, а затем я использую Dispatcher.Invoke перед доступом к TxtBlock3.

Если вы действительно хотите запустить Task в определенном планировщике, передайте TaskScheduler методу Start, как показано ниже.

task1.Start(TaskScheduler.Default);
person Brian Gideon    schedule 04.08.2011
comment
Спасибо, Брайан. Я также добавил еще один вопрос выше - person user774326; 04.08.2011

Используйте Dispatcher.Invoke() для вызова кода, который используется для элементов пользовательского интерфейса из другого потока. Например

string dlCode;
if (blockingCollection.TryTake(out dlCode))
{       
    Dispatcher.Invoke(() =>
    {
         TxtBlock3.Text = dlCode; 
    }
}
person Jalal Said    schedule 04.08.2011
comment
спасибо за ваш ответ, но могу ли я также сделать это с TPL, то есть следовать приведенному выше шаблону? - person user774326; 04.08.2011
comment
Спасибо, но эта часть работает отлично, но проблема в части с TxtBlock3.Text = dlCode - person user774326; 04.08.2011
comment
Используйте Dispatcher.Invoke в коде, который обращается к элементам пользовательского интерфейса из другого потока, например, в вашем TextBoxl3.Text - person Jalal Said; 04.08.2011
comment
Спасибо, я попробую диспетчер и посмотрю, смогу ли я заставить его работать. - person user774326; 04.08.2011