【问题标题】:Serializing async task and async continuation序列化异步任务和异步继续
【发布时间】:2014-11-15 03:40:46
【问题描述】:

我想在后台任务中执行长时间运行的处理。在任务结束时,我想表示它已经完成。所以基本上我有两个要在后台运行的异步任务,一个接一个。

我正在使用延续来执行此操作,但延续是在初始任务完成之前开始的。预期的行为是仅在初始任务完成后继续运行。

下面是一些演示问题的示例代码:

// Setup task and continuation
var task = new Task(async () =>
{
    DebugLog("TASK Starting");
    await Task.Delay(1000);     // actual work here
    DebugLog("TASK Finishing");
});

task.ContinueWith(async (x) =>
{
    DebugLog("CONTINUATION Starting");
    await Task.Delay(100);      // actual work here
    DebugLog("CONTINUATION Ending");
});

task.Start();

DebugLog函数:

static void DebugLog(string s, params object[] args)
{
    string tmp = string.Format(s, args);
    System.Diagnostics.Debug.WriteLine("{0}: {1}", DateTime.Now.ToString("HH:mm:ss.ffff"), tmp);
}

预期输出:

TASK Starting
TASK Finishing
CONTINUATION Starting
CONTINUATION Ending

实际输出:

TASK Starting
CONTINUATION Starting
CONTINUATION Ending
TASK Finishing

再次,我的问题是为什么在完成初始任务之前开始继续?如何在第一个任务完成后继续运行?


解决方法 #1

如果我使初始任务同步,我可以使上述代码按预期工作 - 也就是说,如果我在 Task.Delay 上的 Wait 像这样:

var task = new Task(() =>
{
    DebugLog("TASK Starting");
    Task.Delay(1000).Wait();     // Wait instead of await
    DebugLog("TASK Finishing");
});

由于很多原因,这样使用Wait 是不好的。一个原因是它阻塞了线程,这是我想要避免的。


解决方法 #2

如果我创建任务并将其移动到它自己的函数中,那似乎也可以:

// START task and setup continuation
var task = Test1();
task.ContinueWith(async (x) =>
{
    DebugLog("CONTINUATION Starting");
    await Task.Delay(100);      // actual work here
    DebugLog("CONTINUATION Ending");
});

static public async Task Test1()
{
    DebugLog("TASK Starting");
    await Task.Delay(1000);     // actual work here
    DebugLog("TASK Finishing");
}

上述方法的功劳归于这个有点相关(但不重复)的问题:Use an async callback with Task.ContinueWith


解决方法 #2 比解决方法 #1 更好,如果没有解释为什么上面的初始代码不起作用,我可能会采用这种方法。

【问题讨论】:

    标签: c# async-await task c#-5.0


    【解决方案1】:

    您的原始代码不起作用,因为 async lambda 正在被转换为下面的 async void 方法,该方法没有内置方法来通知任何其他代码它已完成。所以,你看到的是async voidasync Task 之间的区别。这是您应该避免使用async void 的原因之一。

    也就是说,如果可以使用您的代码,请使用Task.Run 而不是Task 构造函数和Start;并使用await 而不是ContinueWith

    var task = Task.Run(async () =>
    {
      DebugLog("TASK Starting");
      await Task.Delay(1000);     // actual work here
      DebugLog("TASK Finishing");
    });
    await task;
    DebugLog("CONTINUATION Starting");
    await Task.Delay(100);      // actual work here
    DebugLog("CONTINUATION Ending");
    

    Task.Runawait 更自然地与 async 代码配合使用。

    【讨论】:

    • -1:原始代码不起作用,因为它没有在必要时使用Unwrap() 方法。创建async void lambda 肯定是不希望的,但也无非是没有使用new Task<Task>(...) 的副作用,所以Unwrap() 可以工作。
    • @280Z28:是的,可以破解原始代码以使用new Task<Task>Unwrap;但是,新代码可以工作因为它可以避免async void lambda。而且它仍然不如基于Task.Run 的解决方案。
    • +1 我最终使用了Task.Run 和原来的延续。感谢您深入了解我现有代码为何不起作用:lambda 变为 async void。当然,当你这么说时,一切都变得更有意义了。那么我为什么要使用延续呢?好吧,虽然和this question有很大关系,但这里解释的太多了。
    【解决方案2】:

    您不应该直接使用 Task 构造函数来启动任务,尤其是在启动异步任务时。如果您想卸载要在后台执行的工作,请改用Task.Run

    var task = Task.Run(async () =>
    {
        DebugLog("TASK Starting");
        await Task.Delay(1000);     // actual work here
        DebugLog("TASK Finishing");
    });
    

    关于延续,最好将其逻辑附加到 lambda 表达式的末尾。但是,如果您坚持使用ContinueWith,则需要使用Unwrap 来获取实际的异步任务并存储它,以便您可以处理异常:

    task = task.ContinueWith(async (x) =>
    {
        DebugLog("CONTINUATION Starting");
        await Task.Delay(100);      // actual work here
        DebugLog("CONTINUATION Ending");
    }).Unwrap();
    

    try
    {
        await task;
    }
    catch
    {
        // handle exceptions
    }
    

    【讨论】:

    • +1 谢谢,Task.Run 正是我所需要的。我从未考虑过它,因为我看到的 MSDN 延续示例 like this one 使用 new Task。我接受斯蒂芬的回答是因为他解释了为什么我上面的代码不起作用:我的初始任务 lambda 变成了 async void 而不是 async Task。那么我为什么要使用延续呢?好吧,虽然和this question有很大关系,但这里解释的太多了。
    【解决方案3】:

    将您的代码更改为

            // Setup task and continuation
            var t1 = new Task(() =>
            {
                Console.WriteLine("TASK Starting");
                Task.Delay(1000).Wait();     // actual work here
                Console.WriteLine("TASK Finishing");
            });
    
            var t2 = t1.ContinueWith((x) =>
            {
                Console.WriteLine("C1");
                Task.Delay(100).Wait();      // actual work here
                Console.WriteLine("C2");
            });
    
            t1.Start();
    
            // Exception will be swallow without the line below
            await Task.WhenAll(t1, t2);
    

    【讨论】:

    • 我通常尽量避免使用Wait,因为它会阻塞正在执行的线程,并且可能会导致线程死锁。有关死锁的更多信息,请参阅this post。该帖子使用Result而不是Wait,但都阻塞了正在执行的线程并且都可能导致死锁。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-25
    • 2022-10-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多