在后台线程上运行“异步”方法

43

我正在尝试从普通方法中运行一个“异步”方法:

public string Prop
{
    get { return _prop; }
    set
    {
        _prop = value;
        RaisePropertyChanged();
    }
}

private async Task<string> GetSomething()
{
    return await new Task<string>( () => {
        Thread.Sleep(2000);
        return "hello world";
    });
}

public void Activate()
{
    GetSomething.ContinueWith(task => Prop = task.Result).Start();
    // ^ exception here
}

抛出的异常是:

不允许在续任务上调用 Start。

这到底意味着什么?我该如何简单地在后台线程上运行我的异步方法,并将结果分派回 UI 线程?

编辑

也尝试过 Task.Wait,但等待永远不会结束:

public void Activate()
{
    Task.Factory.StartNew<string>( () => {
        var task = GetSomething();
        task.Wait();

        // ^ stuck here

        return task.Result;
    }).ContinueWith(task => {
        Prop = task.Result;
    }, TaskScheduler.FromCurrentSynchronizationContext());
    GetSomething.ContinueWith(task => Prop = task.Result).Start();
}
2个回答

64

针对您的例子,需要进行以下修复:

public void Activate()
{
    Task.Factory.StartNew(() =>
    {
        //executes in thread pool.
        return GetSomething(); // returns a Task.
    }) // returns a Task<Task>.
    .Unwrap() // "unwraps" the outer task, returning a proxy
              // for the inner one returned by GetSomething().
    .ContinueWith(task =>
    {
        // executes in UI thread.
        Prop = task.Result;
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

这样可以运行,但是它是老式的方法。

现代的方法是使用Task.Run()asyncawait在后台线程上运行某些内容,并将其分派回UI线程:

async void Activate()
{
    Prop = await Task.Run(() => GetSomething());
}

Task.Run会在线程池线程中启动某个任务。当您使用await等待某些内容时,它会自动返回到启动它的执行上下文中,也就是UI线程。

通常情况下,您不需要调用Start()方法。最好使用async方法、Task.RunTask.Factory.StartNew等自动启动任务的方法。使用awaitContinueWith创建的延续也会在其父任务完成后自动启动。


1
谢谢,这解决了我的困惑。实际上,我痛苦的真正根源是一个异步方法,它使用return new Task而不是return Task.Factory.StartNew - McGarnagle
21
同时,要非常小心async void方法,它们通常不是正确的选择。 - svick
1
请审核我的答案,Task.Factory.StartNew不会在线程池上执行,它会在UI线程上异步执行... - ipavlu
2
await 不会自动返回到启动操作的线程。无法知道 await 是否会导致使用单独的线程,还是使用执行线程(用于继续)。以下是调用 GetFileAsync() 代码部分的两个运行示例: { 线程 ID 28228 LoadBytecodeAsync() 开始 29420 调用 GetFileAsync() 后29420 LoadBytecodeAsync() 开始 29420 调用 GetFileAsync() 后 } 第一次调用 GetFileAsync 会生成一个单独的线程,该线程运行其余的构建代码,包括该部分的第二次运行。 - Gavin Williams
当我尝试这里提出的方法时,我遇到了“异步模块或处理程序在仍有异步操作挂起时完成”的错误。 - Toshihiko

2

使用FromCurrentSynchronizationContext的警告:

好的,Cory知道如何让我重写答案 :)

所以主要问题实际上是FromCurrentSynchronizationContext!每当StartNew或ContinueWith在此类调度程序上运行时,它都会在UI线程上运行。有人可能会想:

好吧,让我们在UI上启动后续操作,更改一些控件,生成一些操作。但从现在开始,TaskScheduler.Current不为null,如果任何控件具有一些事件,这些事件会生成一些StartNew,期望在线程池上运行,那么从那里就会出问题。UI应用程序通常很复杂,难以保证没有任何东西会调用另一个StartNew操作,这里有一个简单的例子:

public partial class Form1 : Form
{
    public static int Counter;
    public static int Cnt => Interlocked.Increment(ref Counter);
    private readonly TextBox _txt = new TextBox();
    public static void WriteTrace(string from) => Trace.WriteLine($"{Cnt}:{from}:{Thread.CurrentThread.Name ?? "ThreadPool"}");

    public Form1()
    {
        InitializeComponent();
        Thread.CurrentThread.Name = "ThreadUI!";

        //this seems to be so nice :)
        _txt.TextChanged += (sender, args) => { TestB(); };

        WriteTrace("Form1"); TestA(); WriteTrace("Form1");
    }
    private void TestA()
    {
        WriteTrace("TestA.Begin");
        Task.Factory.StartNew(() => WriteTrace("TestA.StartNew"))
        .ContinueWith(t =>
        {
            WriteTrace("TestA.ContinuWith");
            _txt.Text = @"TestA has completed!";
        }, TaskScheduler.FromCurrentSynchronizationContext());
        WriteTrace("TestA.End");
    }
    private void TestB()
    {
        WriteTrace("TestB.Begin");
        Task.Factory.StartNew(() => WriteTrace("TestB.StartNew - expected ThreadPool"))
        .ContinueWith(t => WriteTrace("TestB.ContinueWith1 should be ThreadPool"))
        .ContinueWith(t => WriteTrace("TestB.ContinueWith2"));
        WriteTrace("TestB.End");
    }
}
  1. Form1:ThreadUI! - 好的
  2. TestA.Begin:ThreadUI! - 好的
  3. TestA.End:ThreadUI! - 好的
  4. Form1:ThreadUI! - 好的
  5. TestA.StartNew:ThreadPool - 好的
  6. TestA.ContinuWith:ThreadUI! - 好的
  7. TestB.Begin:ThreadUI! - 好的
  8. TestB.End:ThreadUI! - 好的
  9. TestB.StartNew - 预期是 ThreadPool:ThreadUI! - 可能出乎意料!
  10. TestB.ContinueWith1 应该是 ThreadPool:ThreadUI! - 可能出乎意料!
  11. TestB.ContinueWith2:ThreadUI! - 好的

请注意,由以下方法返回的任务:

  1. 异步方法,
  2. Task.Factory.StartNew,
  3. Task.Run,

不能被启动!它们已经是热任务了...


1
如果当前没有正在执行的任务,则StartNewContinueWith将使用默认(线程池)调度程序。 FromCurrentSynchronizationContext从不是默认值。总的来说,您的信息是正确的,但是,如果您不知道代码在哪里执行,最好明确要求默认调度程序。 - Cory Nelson
好的,你是对的。StartNew和ContinueWith与Current一起使用,如果为null,则使用Default,因此ThreadPool除非像FromCurrentSynchronizationContext这样指定。任何涉及FromCurrentSynchronizationContext的危险在于所有后续代码现在都有一个不为空的Current,即使ContinueWith正在UI线程上运行并且触摸任何东西也可以启动另一个相同的StartNew,在不同的线程上运行,它将不再在ThreadPool上运行...答案中有例子... - ipavlu

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