【问题标题】:WPF, TPL, Producer/Consumer Pattern - Wrong Thread ErrorWPF、TPL、生产者/消费者模式 - 错误的线程错误
【发布时间】:2011-10-20 01:44:35
【问题描述】:

我是 TPL 和 WPf 的新手,遇到以下问题。 我尝试在无限循环中下载一个站点(这里只有一个 for 循环)和 将其添加到队列中。下一个任务将其取出并显示在文本块中。 但是,尽管我认为我正确使用了 TaskScheduler,但我似乎没有为 UI 找到正确的线程。

感谢您的帮助!

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 bytes
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback 回调, 对象状态, bool ignoreSyncCtx) + 0xb0 bytes
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 字节
[本机到托管转换]

   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 似乎引起了问题。没有错误信息。看起来更像是一个紧密的循环。当然,我不知道为什么? 你会这么好心,并再次指出我正确的方向。 请记住,我已经做了大约 6 个月的 C# 和一周的 TPL,否则我不会再问你了。但是有了这么多经验... 再次感谢!

布莱恩密码:

var task1 = new Task( 
  (obj) => 

为什么需要obj

private void Button1_Click(object sender, RoutedEventArgs e)
        {

TaskScheduler uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); BlockingCollectionblockingCollection = new BlockingCollection(); CancellationTokenSource cts = new 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);

        }

【问题讨论】:

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

标签: c# .net wpf multithreading task-parallel-library


【解决方案1】:

您的代码存在一些问题。

  • 接受TaskScheduler 的Task ctor 没有过载。您实际所做的是将 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);

【讨论】:

  • 谢谢布赖恩。我还在上面添加了另一个问题
【解决方案2】:

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

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

taskCP.Start(uiTaskScheduler);

更新

好的,所以现在更新代码的问题是您已经有效地在 UI(调度程序)线程上安排了一个Task,该线程正坐在那里以紧密的循环读取。

现在代码已经重新编写,很明显您确实想在 UI 线程上安排 task2。如果您想将通知从那里推送到 UI,您可以按照另一个答案的建议调用 Dispatcher::BeginInvoke,或者您可以使用循环内的 uiTaskScheduler 启动一个新任务。恕我直言,调用 Dispatcher::BeginInvoke 开销更少,代码更清晰,所以我建议这样做。

【讨论】:

  • 谢谢。我添加了另一个并用 Task.Factory.StartNew 重写了它
  • 好的,现在我明白了。我对任务本身如何与 Dispatcher 结合使用是一种误解。感谢您帮助改进我的代码并向我展示了我必须努力和实践才能变得更好。
  • 我有一种感觉,我们可能会从一开始就在这里结束,但你真正的问题是为什么你的任务没有在 UI 线程上执行,我真的很想深入了解它您问题的“正确”答案。很高兴为您提供帮助!
  • 如果没有足够的知识和经验,有时很难提出一个准确的问题。谈话中会出现一些问题和问题。我对泛型和 Intellisense 也有疑问。这就是为什么我也感谢你的耐心和不理解!再次感谢!
【解决方案3】:

您可以使用 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 代码不起作用。
【解决方案4】:

使用Dispatcher.Invoke() 调用用于来自不同线程的 UI 元素的代码。例如

string dlCode;
if (blockingCollection.TryTake(out dlCode))
{       
    Dispatcher.Invoke(() =>
    {
         TxtBlock3.Text = dlCode; 
    }
}

【讨论】:

  • 谢谢你的回答,但我也可以用 TPL 来做,也就是说按照上面的模式?
  • 谢谢,但那部分工作得很好,但问题是 TxtBlock3.Text = dlCode 的部分
  • 在代码中使用 Dispatcher.Invoke 从另一个线程访问 UI 元素,例如您的 TextBoxl3.Text
  • 谢谢,我会试试调度程序,看看我能不能完成这个工作。
猜你喜欢
  • 2017-03-10
  • 2021-07-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多