UI线程上的任务继续


214

是否有一种“标准”方法来指定任务继续应该在创建初始任务的线程上运行?

目前,我有下面的代码-它正在工作,但是跟踪调度程序并创建第二个Action似乎是不必要的开销。

dispatcher = Dispatcher.CurrentDispatcher;
Task task = Task.Factory.StartNew(() =>
{
    DoLongRunningWork();
});

Task UITask= task.ContinueWith(() =>
{
    dispatcher.Invoke(new Action(() =>
    {
        this.TextBlock1.Text = "Complete"; 
    }
});

在您的示例中,您可以使用Control.Invoke(Action),即。TextBlock1.Invoke而不是dispatcher.Invoke
上校恐慌

2
感谢@ColonelPanic,但是我使用的是WPF(已标记),而不是winforms。
格雷格·桑索姆

Answers:


352

调用延续TaskScheduler.FromCurrentSynchronizationContext()

    Task UITask= task.ContinueWith(() =>
    {
     this.TextBlock1.Text = "Complete"; 
    }, TaskScheduler.FromCurrentSynchronizationContext());

仅当当前执行上下文在UI线程上时,这才适用。


39
仅当当前执行上下文在UI线程上时,此命令才有效。如果将此代码放在另一个Task中,则会得到InvalidOperationException(请
参见“

3
在.NET 4.5中,Johan Larsson的答案应该用作UI线程上的任务继续的标准方法。只需编写:await Task.Run(DoLongRunningWork); this.TextBlock1.Text =“完成”; 另请参阅:blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
Marcel W

1
谢谢你救我一命。我花了几个小时来弄清楚如何在await / ContinueWith中调用主线程。对于其他所有人来说,如何使用Google Firebase SDK for Unity仍然存在相同的问题,这是一种可行的方法。
CHaP

1
@MarcelW- await是一个很好的模式-但仅当您位于async上下文(例如声明的方法async)内时。如果没有,仍然有必要做这样的回答。
制造商史蒂夫,

33

使用异步,您只需执行以下操作:

await Task.Run(() => do some stuff);
// continue doing stuff on the same context as before.
// while it is the default it is nice to be explicit about it with:
await Task.Run(() => do some stuff).ConfigureAwait(true);

然而:

await Task.Run(() => do some stuff).ConfigureAwait(false);
// continue doing stuff on the same thread as the task finished on.

2
false版本下的注释使我感到困惑。我认为false这可能会在其他线程上继续。
制造商史蒂夫

1
@ToolmakerSteve取决于您正在考虑的线程。Task.Run使用的辅助线程还是调用者线程?请记住,“完成任务的相同线程”是指工作线程(避免“切换”线程)。另外,ConfigureAwait(true)不能保证控件返回相同的线程,而仅返回相同的上下文(尽管区别可能并不重要)。
Max Barraclough

@MaxBarraclough-谢谢,我误读了“相同线程”的含义。通过使用碰巧正在运行的任何线程[执行“执行某些任务”任务],可以避免在线程之间切换,从而最大程度地提高性能,这一点对我来说很清楚。
制造商

1
问题没有指定在async方法内部(使用,这是必需的await)。什么时候await没有答案?
制造商史蒂夫

22

如果您有返回值,则需要发送给UI,您可以使用如下通用版本:

在我的情况下,这是从MVVM ViewModel调用的。

var updateManifest = Task<ShippingManifest>.Run(() =>
    {
        Thread.Sleep(5000);  // prove it's really working!

        // GenerateManifest calls service and returns 'ShippingManifest' object 
        return GenerateManifest();  
    })

    .ContinueWith(manifest =>
    {
        // MVVM property
        this.ShippingManifest = manifest.Result;

        // or if you are not using MVVM...
        // txtShippingManifest.Text = manifest.Result.ToString();    

        System.Diagnostics.Debug.WriteLine("UI manifest updated - " + DateTime.Now);

    }, TaskScheduler.FromCurrentSynchronizationContext());

我猜在GenerateManifest是拼写错误之前=。
塞巴斯蒂安·F。

是的-现在走了!谢谢。
Simon_Weaver 2015年

11

我只想添加此版本,因为这是一个有用的线程,我认为这是一个非常简单的实现。如果使用多线程应用程序,我已经在各种类型中多次使用了此方法:

 Task.Factory.StartNew(() =>
      {
        DoLongRunningWork();
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
              { txt.Text = "Complete"; }));
      });

2
不要拒绝投票,因为在某些情况下这是一个可行的解决方案;但是,公认的答案要好得多。它与技术无关(不是TaskSchedulerBCL的一部分,Dispatcher不是BCL的一部分),并且由于不必担心任何即发即弃的异步操作(例如)而可以用于组成复杂的任务链BeginInvoke
Kirill Shlenskiy 2014年

@Kirill可以扩展一点,因为如果使用WinForms的WPF,某些SO线程会一致地宣布调度程序为正确的方法:一个人可以异步(使用BeginInvoke)或同步(Invoke)调用GUI更新,尽管通常异步使用它是因为不想仅为了GUI更新而阻塞后台线程。FromCurrentSynchronizationContext是否不会以与调度程序相同的方式将继续任务放入主线程消息队列中?
教务长

1
是的,但是OP肯定在询问WPF(并对其进行了标记),并且不想保留对任何调度程序的引用(并且我也假设任何同步上下文-您只能从主线程获取此消息,并且必须将其引用存储在某处)。这就是为什么我喜欢我发布的解决方案的原因:内置了一个线程安全的静态引用,不需要任何这些。我认为这在WPF上下文中非常有用。
迪恩

3
只是想加强我的最后一句话:开发人员不仅必须存储同步上下文,而且他/她必须知道只能从主线程获得该上下文。这个问题是数十个SO问题引起的混乱:人们一直在尝试从工作线程中获取该问题。如果他们的代码本身已移入工作线程,则由于此问题而失败。因此,由于WPF的盛行,在这个普遍存在的问题中肯定应该对此进行澄清。
教务长

1
...尽管如此,Dean关于[已接受的答案]需要跟踪同步上下文(如果代码可能不在主线程中)的观察很重要,请注意,避免这样做是有益的。
制造商

1

因为我一直在寻找在Task.Run调用后在ui线程上做事的好方法,因此可以通过google进行搜索-使用以下代码,您可以使用该代码await再次返回UI线程。

我希望这可以帮助别人。

public static class UI
{
    public static DispatcherAwaiter Thread => new DispatcherAwaiter();
}

public struct DispatcherAwaiter : INotifyCompletion
{
    public bool IsCompleted => Application.Current.Dispatcher.CheckAccess();

    public void OnCompleted(Action continuation) => Application.Current.Dispatcher.Invoke(continuation);

    public void GetResult() { }

    public DispatcherAwaiter GetAwaiter()
    {
        return this;
    }
}

用法:

... code which is executed on the background thread...
await UI.Thread;
... code which will be run in the application dispatcher (ui thread) ...

非常聪明!虽然很不直观。我建议static上课UI
西奥多·祖里亚斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.