【问题标题】:update UI in Task using TaskScheduler.FromCurrentSynchronizationContext使用 TaskScheduler.FromCurrentSynchronizationContext 更新 Task 中的 UI
【发布时间】:2013-07-02 05:47:39
【问题描述】:

我想使用Task 向列表框添加一些文本,我只需使用一个按钮并将此代码放置在点击事件中:

TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.StartNew(() =>
{
    for (int i = 0; i < 10; i++)
    {
        listBox1.Items.Add("Number cities in problem = " + i.ToString());
        System.Threading.Thread.Sleep(1000);
    }
}, CancellationToken.None, TaskCreationOptions.None, uiScheduler);

但在 for 循环结束之前它不起作用并且 UI 被锁定。

问题出在哪里?

谢谢:)

【问题讨论】:

    标签: c# multithreading c#-4.0 task


    【解决方案1】:

    您可以在线程中完成所有工作,但是当您需要更新 UI 时,请使用调度程序:

    Task.Factory.StartNew(() =>
    {
      for (int i = 0; i < 10; i++)
      {
         Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, 
            new Action(() => {listBox1.Items.Add("Number cities in problem = " + i.ToString()); }));
         System.Threading.Thread.Sleep(1000);
      }
    });
    

    【讨论】:

    • 以我的拙见,这应该是公认的答案
    • 如果你碰巧在做一个 Xamarin 跨平台移动项目,等效的命令是Xamarin.Forms.Device.BeginInvokeOnMainThread
    • @IgnacioSolerGarcia - 但我不相信这能解决问题。 UI 线程正在更新项目列表,但是 thread.sleep 仍然会使 UI 冻结。
    • @AlvinfromDiaspar:Sleep(1000) 是在任务上执行的,所以我看不出它是如何阻塞 UI 的。你试过代码了吗?
    【解决方案2】:

    问题出在哪里?

    嗯,你明确地说你想在 UI 线程中执行任务...然后你在任务中休眠,所以它阻塞了 UI 线程。您如何期望在 UI 线程中,但 Thread.Sleep 不会 引起问题?

    如果您可以使用 C# 5 和 async/await,那会让事情变得更容易:

    private static async Task ShowCitiesAsync()
    {
        for (int i = 0; i < 10; i++)
        {
            listBox1.Items.Add("Number cities in problem = " + i);
            await Task.Delay(1000);
        }
    }
    

    如果您不能使用 C# 5(如您的标签所建议的那样),那就更棘手了。你最好使用Timer

    // Note: you probably want System.Windows.Forms.Timer, so that it
    // will automatically fire on the UI thread.
    Timer timer = new Timer { Interval = 1000; }
    int i = 0;
    timer.Tick += delegate
    {
        listBox1.Items.Add("Number cities in problem = " + i);
        i++;
        if (i == 10)
        {
            timer.Stop();
            timer.Dispose();
        }
    };
    timer.Start();
    

    如您所见,它非常丑陋...而且它假定您实际上不想在 UI 更新之间进行任何工作

    另一种选择是使用BackgroundWorker 在不同的线程上模拟您长时间运行的任务(此时正在休眠),然后使用ReportProgress 返回 UI 线程以添加列表项。

    【讨论】:

    • 谢谢,但我目前正在使用C#-4 我能做什么?在我的主程序中我没有使用Sleep 我有一些方法是long running,我在执行后添加了项目它们在列表框中,例如Step 1 Completed...,但此消息在运行之前不会显示
    • @Kerezo:查看我的编辑以了解两种选择。但我们真的不知道更大的图景是什么 - 大概你会在某个时候想做除了睡觉以外的事情。
    【解决方案3】:

    只是为 c# 4.0 提供另一种解决方案。这类似于@Debora 和@Jay(好吧,如果您忘记了 while(true)... 只是在谈论 BeginInvoke)解决方案,但完全基于 TPL 并且更接近使用 async/ 生成的代码等待:

    TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < 10; i++)
        {
            Task.Factory.StartNew(() =>
            {
                 listBox1.Items.Add("Number cities in problem = " + i.ToString());
            }, CancellationToken.None, TaskCreationOptions.None, uiScheduler);
    
            System.Threading.Thread.Sleep(1000);
        }
    }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
    

    您的工作任务应使用默认的 TaskScheduler(将使用 ThreadPool)进行调度,并在需要更新 UI 时使用 uiScheduler 回调到 UI 线程。 请记住,这是一个示例实现,此示例可能存在一些问题,例如,内部任务计划在 UI 线程上执行,但调用任务并未等待,因此 sleep 将实际运行当内部任务正在运行时。 不要等待任务也很重要,否则可能会出现死锁(内部任务试图在等待外部任务的 UI 线程上运行)。

    我通常使用 uiScheduler on continuation 任务向 UI 提供数据。在你的情况下,它可能是这样的:

    TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    Task.Factory.StartNew(() =>
    {
        //load data or perform work on background thread
        var itemList = new List<int>();
        for (int i = 0; i < 10; i++)
        {
            itemList.Add(i);
            System.Threading.Thread.Sleep(1000);
        }
        return itemList;
    }).ContinueWith((itemList) => 
    {
       //get the data from the previous task a continue the execution on the UI thread
       foreach(var item in itemList)
       {
          listBox1.Items.Add("Number cities in problem = " + item.ToString());
       }
    }, CancellationToken.None, TaskCreationOptions.None, uiScheduler);
    

    生成的编译代码将与使用 async/await 生成的代码非常相似(如果不相等)

    【讨论】:

      【解决方案4】:

      根据这个帖子中的建议,我想出了以下解决方案:

          public TaskGUIUpdater(Form1 form1, TaskDataGenerator taskDataGenerator)
          {
              Task.Factory.StartNew(() => {
                  while (true)
                  {
                      form1.BeginInvoke(new Action(() => {
                          form1.UpdateGUI(taskDataGenerator.Counter, taskDataGenerator.RandomData);
                      }));
      
                      Thread.Sleep(1000);
                  }
              });
          }
      

      TaskGUIUpdater 是主窗体之外的类中的构造函数。它需要对需要更新的表单的引用,以及需要从中获取数据的任务的引用。

      form1.UpdateGUI 方法将您想要设置的任何数据设置为 form1,然后简单地将其设置为 form1。

      【讨论】:

      • 请看看为什么while(true) and Thread.Sleep together are "evil",你还缺少CancellationToken 或类似的东西来控制一个break-scenario。
      • 上面的代码做了它需要做的事情。它启动一次并运行直到程序关闭。不需要取消令牌,因为我从不打算结束任务。这是一小段代码,不会占用任何重要资源。它在后台运行并且是轻量级的。您链接到的线程解释说 Thread.Sleep 不是很精确,它占用 1Mb 的内存(= 没什么大不了的)。链接到的文章解释说,在 while 循环中使用 Thread.Sleep 是邪恶的,因为条件检查将使 cpu 最大化。随时提供更好的解决方案。
      • 在阅读 cmets 之前,我实际上并没有注意到 while (true)。为什么要让这个任务永远运行?你实际上是在浪费资源,每秒循环一次,在主线程上安排工作,并可能强制重绘应用程序。为什么不只在数据(taskDataGenerator.Count 和/或 taskDataGenerator.RandomData)发生变化时(且仅在)发生变化时调用 BeginInvoke?您将获得“实时”更新(而不是等待 1 秒),并且仅在数据更改时才在 UI 上运行更新,从而避免不必要的应用重绘。
      • 该任务需要“永远”运行,因为它需要在应用程序的整个生命周期中运行。我想将资源使用保持在最低限度,因此延迟 1 秒,这是 CPU 时间的永恒。我确信有更好的解决方案,但是当我需要它时,这个东西就起作用了。要么接受,要么离开它。我不再参与这个项目。
      猜你喜欢
      • 1970-01-01
      • 2016-11-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多