为什么连续使用 ContinueWith 到 运行 任务不起作用?

Why doesn't it work to use ContinueWith to run task sequentially?

我的目标是在 "Task1" 之后开始 "Task2"。起初我写了像下面的 "Code1" 这样的代码,但是它不起作用(Task2 在 Task1 完成之前开始)。所以我搜索了 Whosebug 并按照 the existing answer 的建议修改了我的代码,例如下面的 "Code2"。我想知道为什么 "Code1" 不起作用。

代码1

    static void Main(string[] args)
    {
        var p = new Program();
        p.Test2();
        Console.ReadKey();
    }

    void Test2()
    {
        Task.Factory.StartNew(async () =>
        {
            await Task1();
        }).ContinueWith((t) => {
            Task2();
        });
    }

    async Task Task1()
    {
        Debug.WriteLine("Task 1 starting....");
        await LongTask();
        Debug.WriteLine("Task 1 done");
    }

    Task LongTask()
    {
        return Task.Factory.StartNew(() =>
        {
            Thread.Sleep(3000);
        });            
    }

    void Task2()
    {
        Debug.WriteLine("Task 2");
    }

代码2

        Task.Factory.StartNew(async () =>
        {
            await Task1();
            Task2();
        }).ContinueWith((t) => {
            //Task2();
        });

因为当您 运行 任务时 Task.Factory.StartNew(async () => ... 它 returns Task<Task> (任务的任务)。第一个任务终止而无需等待内部 Task.

为了防止这种情况,您可以使用 Unwrap 方法:

Task.Factory.StartNew(async () =>
{
    await Task1();
})
.Unwrap()
.ContinueWith((t) => {
    Task2();
});

那个StartNew会returnTask<Task>,看来你是想在内部任务完成后继续执行。这就是为什么我们需要 Unwrap。 它“解开”作为外部任务结果的 returned 的内部任务。在任务上调用 Unwrap 会返回一个新任务(我们通常将其称为代理),它表示内部任务的最终完成。然后,我们正在为内部任务添加延续。

但是,由于 Task.Run 会自动展开,您可以在这种情况下使用 Task.Run

Task.Run(async () =>
{
    await Task1();
})
.ContinueWith((t) => {
    Task2();
});

可以简化为:

Task.Run(async () =>
{
    await Task1();
    Task2();
});

关于Task.RunTask.Factory.StartNew之间差异的详细信息:

从 .Net 团队的工程师 Stephen Toub 那里了解更多 hereTask.Run 情况下的决定原因:

Because we expect it to be so common for folks to want to offload work to the ThreadPool, and for that work to use async/await, we decided to build this unwrapping functionality into Task.Run.

顺便说一句,正如 Stephen 所建议的那样,在大多数情况下只需尝试使用 Task.Run,但这绝不会使 Task.Factory.StartNew 过时,仍然有一些地方 Task.Factory.StartNew 必须被使用:

Task.Factory.StartNew still has many important (albeit more advanced) uses. You get to control TaskCreationOptions for how the task behaves. You get to control the scheduler for where the task should be queued to and run. You get to use overloads that accept object state, which for performance-sensitive code paths can be used to avoid closures and the corresponding allocations. For the simple cases, though, Task.Run is your friend.

Task.Factory.StartNew 不理解异步 lambda。对于 Task.Factory.StartNew,您的 lambda 只是返回 Task 的函数。它不会自动等待该任务。另外,请注意,在现代 C# 中不鼓励使用 Task.Factory.StarNewContinueWith,因为它们很难正确使用(请阅读 Stephen Cleary 的 "StartNew is Dangerous")。仅当您离不开它们时才应使用 Task.Factory.StarNewContinueWith

相反,您可以再使用一次 async/await:

async Task Test2()
{
    await Task1();
    Task2();
}

Task.Run

async Task Test2()
{
    return Task.Run(await () => 
    {
        await Task1();
        Task2();
    });
}

如果您想确保异步 Task1() 在后台线程上启动,第二种方法可能会很方便。