Task.WaitAll无法等待任务完成

20

在尝试理解 C# 中新的(也许现在并不那么新了,但对我来说是新的)异步编程 Task 时,我遇到了一个问题,花了我一些时间才解决,但我不确定为什么会出现这个问题。

我已经解决了这个问题,但仍然不确定为什么它一开始成为了问题。只是想分享我的经验,以防万一有人遇到相同的情况。

如果任何大师能告诉我问题的原因,那将非常棒并且非常感激。我总是喜欢知道为什么某件事情不起作用!

我创建了一个测试任务,如下所示:

Random rng = new Random((int)DateTime.UtcNow.Ticks);
int delay = rng.Next(1500, 15000);
Task<Task<object>> testTask = Task.Factory.StartNew<Task<object>>(
    async (obj) =>
        {
            DateTime startTime = DateTime.Now;
            Console.WriteLine("{0} - Starting test task with delay of {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (int)obj);
            await Task.Delay((int)obj);
            Console.WriteLine("{0} - Test task finished after {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (DateTime.Now - startTime).TotalMilliseconds);
            return obj;
        },
        delay
    );
Task<Task<object>>[] tasks = new Task<Task<object>>[] { testTask };

Task.WaitAll(tasks);
Console.WriteLine("{0} - Finished waiting.", DateTime.Now.ToString("h:mm:ss.ffff"));

// make console stay open till user presses enter
Console.ReadLine();

然后我运行了应用程序,查看它输出了什么。以下是一些示例输出:

6:06:15.5661 - 带有3053ms延迟的测试任务开始执行。
6:06:15.5662 - 等待完成。
6:06:18.5743 - 测试任务在3063.235ms后完成。

正如您所看到的那样,Task.WaitAll(tasks);语句并没有做太多事情。在继续执行之前,它仅等待了总共1毫秒。

我在下面回答了自己的“问题”-但是像我说的那样,如果有比我更有知识的人能解释为什么这不起作用,请这样做! (我认为可能与执行到达await操作符时执行“步出”方法,然后在等待完成后再次“步入”方法有关...但我可能是错的)


为什么要使用 new Random((int)DateTime.UtcNow.Ticks)?为什么不直接使用 new Random(),因为它们本质上是相同的。 - Enigmativity
并不是说这对于这个问题有多重要或相关,而只是为了看看是否有任何差别。我没有注意到调用无参数构造函数会有任何差别,所以从现在开始我就这样做了。如果你不尝试新事物,就不会学到任何东西。编程对我来说是一种爱好,除了Karel the Robot、Pascal和SQL之外,我没有接受过任何正式的教育,而那已经是13年前的事情了,所以对我来说很重要的是去摸索、尝试、打破、弄清楚并学习。 - cjk84
你最好下载其中一个免费的.NET反编译器并查看源代码。 - Enigmativity
如果像你所说的那样,调用无参构造函数会做同样的事情,那么我几乎看不出有什么害处,除了可能让我多按几次键盘,接近 R.S.I. 的病例,但并不像你那样毫无意义地和居高临下地指出来! - cjk84
我绝对不是在居高临下。我见过很多人试图使用构造函数来解决产生非随机数的问题。但这并不起作用。我只是想知道你是否希望这样做。 - Enigmativity
4个回答

27

你应该避免在 async-await 中使用 Task.Factory.StartNew,应该改用 Task.Run

async 方法返回一个 Task<T>,async 委托也是如此。而 Task.Factory.StartNew 也返回一个 Task<T>,其结果是委托参数的结果。因此,当它们一起使用时,会返回一个 Task<Task<T>>>

Task<Task<T>> 的作用就是执行委托,直到有一个任务要返回,这时达到第一个 await 时就会返回。如果你只等待那个任务完成,你并没有等待整个方法,只等待第一个 await 之前的部分。

你可以通过使用 Task.Unwrap 来解决这个问题,它创建一个表示该 Task<Task<T>>>Task<T>

Task<Task> wrapperTask = Task.Factory.StartNew(...);
Task actualTask = wrapperTask.Unwrap();
Task.WaitAll(actualTask);

1
还有一个问题要问您:我猜测因为我删除了async/await,结果任务从Task<Task<object>>变成了Task<object>,它之所以能正常工作纯粹是因为这个原因,而将Task.Delay更改为Thread.Sleep与此无关。对吗? - cjk84
2
@cjk84 是的。如果你去掉了async-await并使用Task.Delay(...).Wait(),它会有相同的结果。 - i3arnon
waitall(x).wait(timeout)会等待任务完成,即使超时发生了?还是会继续执行? - Hassan Faghihi

11

你的代码问题在于有两个任务同时在运行。其中一个是通过 Task.Factory.StartNew 调用执行的匿名函数,在线程池上执行。但是,你的匿名函数编译后生成了一个嵌套的任务,代表其异步操作的完成。当你等待你的 Task<Task<object>> 时,你只等待外部任务。要等待内部任务,你应该使用 Task.Run 而不是 Task.Factory.StartNew,因为它会自动展开你的内部任务:

Random rng = new Random((int)DateTime.UtcNow.Ticks);
int delay = rng.Next(1500, 15000);
Task<int> testTask = Task.Run(
    async () =>
    {
        DateTime startTime = DateTime.Now;
        Console.WriteLine("{0} - Starting test task with delay of {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), delay);
        await Task.Delay(delay);
        Console.WriteLine("{0} - Test task finished after {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (DateTime.Now - startTime).TotalMilliseconds);
        return delay;
    });
Task<int>[] tasks = new[] { testTask };

Task.WaitAll(tasks);
Console.WriteLine("{0} - Finished waiting.", DateTime.Now.ToString("h:mm:ss.ffff"));

// make console stay open till user presses enter
Console.ReadLine();

3

这里,Task.WaitAll 等待外部任务而不是内部任务。使用 Task.Run 可以避免嵌套任务,这是最佳实践解决方案。另一种解决方案是同时等待内部任务。例如:

Task<object> testTask = Task.Factory.StartNew(
    async (obj) =>
        {
            ...
        }
    ).Unwrap();

或者:

testTask.Wait();
testTask.Result.Wait();

啊,我一直在想为什么它不让我添加async关键字,直到我嵌套了任务。谢谢你的信息! - cjk84
如果您将异步任务添加到Task.Factory中,请不要忘记在之前添加await: await Task.Factory.StartNew(async () => .... - Lev K.

1

经过漫长的调试和折磨,我最终决定放弃异步lambda,改用System.Threading.Thread.Sleep方法,看看是否会有所不同。

新代码如下:

Random rng = new Random((int)DateTime.UtcNow.Ticks);
int delay = rng.Next(1500, 15000);
Task<object> testTask = Task.Factory.StartNew<object>(
    (obj) =>
        {
            DateTime startTime = DateTime.Now;
            Console.WriteLine("{0} - Starting test task with delay of {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (int)obj);
            System.Threading.Thread.Sleep((int)obj);
            Console.WriteLine("{0} - Test task finished after {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (DateTime.Now - startTime).TotalMilliseconds);
            return obj;
        },
        delay
    );
Task<object>[] tasks = new Task<object>[] { testTask };

Task.WaitAll(tasks);
Console.WriteLine("{0} - Finished waiting.", DateTime.Now.ToString("h:mm:ss.ffff"));

// make console stay open till user presses enter
Console.ReadLine();

注意:由于从lambda方法中删除了async关键字,因此任务类型现在可以简单地是Task<object>而不是Task<Task<object>> - 您可以在上面的代码中看到这个变化。
嗨!它奏效了!在任务完成后我收到了“完成等待”的消息。
唉,我记得在某个地方读到过你不应该在Task代码中使用System.Threading.Thread.Sleep()。我忘记为什么;但因为这只是用于测试,而大多数任务实际上会做一些需要时间的事情而不是假装正在做一些需要时间的事情,所以这应该不是一个问题。
希望这能帮助一些人。我肯定不是世界上最好的程序员(甚至不接近),我的代码可能不太好,但如果它能帮助某个人,那太棒了!
感谢您的阅读。

编辑:其他回答解释了我遇到的问题,而这个答案只是无意中解决了问题。更改为Thread.Sleep(x) 没有任何效果。感谢所有回答和帮助我的人!


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