WPF、TPL、生产者/消费者模式 - 错误的线程错误

3

我对TPL和WPF都很陌生,遇到了以下问题。 我尝试在一个无限循环中(这里只是一个for循环)下载一个网站并将其添加到队列中。下一个任务将其取出并在Textblock中显示。但是,我似乎无法获得正确的UI线程,尽管我认为我已经正确使用了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 executionContext, System.Threading.ContextCallback callback, object state, 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.Dispatch() + 0x147字节 mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() + 0x2d字节 [Native to Managed Transition]
   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 = new CancellationTokenSource();

任务调度程序uiTaskScheduler = 从当前同步上下文中获取的任务调度程序(); 阻塞集合 blockingCollection = 新建阻塞集合(); 取消标记源 cts = 新建取消标记源();
            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);

        }

请问您能否粘贴完整的堆栈跟踪信息?此外,该方法最初是否从 UI 线程调用以启动? - Drew Marsh
是的,实际上它们是从UI线程启动的。这都声明在以下事件中:private void Button1_Click(object sender, RoutedEventArgs e) { }我已经粘贴了堆栈。 - user774326
抱歉,能否粘贴完整的异常详细信息(即exception.ToString())?这应该绝对可行。调用Dispatcher.[Begin]Invoke不是必需的。另外,如果工作被取消,您将在非UI计划任务(task1)中设置TextBlock2.Text存在错误。 - Drew Marsh
我已经添加了。希望这是正确的堆栈跟踪。谢谢! - user774326
4个回答

3

好的,实际上我再次查看了您的代码,问题很简单:您正在使用手动构造带有状态对象的Task实例。没有带有TaskScheduler参数的构造函数重载。

通常人们使用Task.Factory.StartNew,所以我甚至没有注意到您是手动构造Tasks。当您手动构造Task实例时,指定要运行它们的调度程序的正确方式是使用带有TaskScheduler参数的Start方法重载。所以在您的情况下,需要这样做:

taskCP.Start(uiTaskScheduler);

更新

好的,现在你更新后的代码存在问题,实际上你已经在UI(调度程序)线程上安排了一个紧密循环读取的Task

现在重新设计代码后,很明显你不希望将task2定时在UI线程上运行。如果你想从那里向UI推送通知,你可以像另一个答案所建议的那样调用Dispatcher::BeginInvoke,或在循环内部使用uiTaskScheduler启动一个新的任务。在我看来,调用Dispatcher::BeginInvoke的开销更小,代码清晰易懂,所以我建议只是使用它。


谢谢。我添加了另一个并使用Task.Factory.StartNew进行了重写。 - user774326
好的,现在我明白了。这是我对任务本身如何与调度程序结合工作的一种误解。感谢您帮助我改进代码,并向我展示需要努力和练习以变得更好的方向。 - user774326
我一开始就有预感我们可能会来到这里,但你真正的问题是为什么你的任务没有在UI线程上执行,我真的想弄清楚这个问题,以便给出你问题的“正确”答案。虽然很高兴能帮忙! - Drew Marsh
有时候,如果缺乏足够的知识和经验,很难提出精确的问题。一些问题和困难会在交谈中出现。我也曾遇到过泛型和智能感知的问题。 因此,我非常感谢您的耐心和理解!再次感谢! - user774326

2
您的代码存在一些问题:
  • 任务构造函数没有重载可以接受TaskScheduler。实际上,您将TaskScheduler传递给了state参数,然后在lambda表达式中选择了obj变量。
  • 由于上面的原因,taskCP实际上是在默认调度程序上运行,而不是uiScheduler上运行。
  • 由于上述原因,taskCP试图通过修改TxtBlock3从非UI线程访问UI元素。
  • 同样,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 在默认调度程序上运行,然后在访问 TxtBlock3 之前使用 Dispatcher.Invoke
如果您真的想在特定的调度程序上启动一个 Task,则可以像以下代码一样将 TaskScheduler 传递给 Start 方法。
task1.Start(TaskScheduler.Default);

谢谢Brian。我也在上面添加了另一个问题。 - user774326

2
您可以使用Dispatcher来访问UI线程,例如:
Dispatcher.BeginInvoke(new Action(()=>{TxtBlock2.Text = "Signalling production end";}));

由于任务在UI线程之外的另一个线程中运行,Dispatcher为您提供了访问UI线程的机会。MSDN对此进行了很好的解释,请查看备注部分: http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.aspx 希望这有所帮助。

调度程序代码运行得非常好:)。谢谢 我仍然不明白为什么我的TPL代码不起作用。 - user774326

0
使用 Dispatcher.Invoke() 方法从不同的线程中调用用于 UI 元素的代码。例如:
string dlCode;
if (blockingCollection.TryTake(out dlCode))
{       
    Dispatcher.Invoke(() =>
    {
         TxtBlock3.Text = dlCode; 
    }
}

谢谢您的回答,但我也可以使用TPL来完成吗?这意味着遵循上述模式吗? - user774326
谢谢,但那部分完美运作,问题出在TxtBlock3.Text = dlCode这一部分。 - user774326
在访问UI元素的代码中,例如TextBoxl3.Text,如果来自另一个线程,请使用Dispatcher.Invoke - Jalal Said
谢谢,我会尝试使用调度程序,看看能否让它正常工作。 - user774326

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接