Task.WaitAll不等待任务完成

时间:2021-04-02 02:21:35

While trying to figure out the new (maybe not so new now, but new to me, anyway) Task asynchronous programming in C#, I ran into a problem that took me a bit to figure out, and I'm not sure why.

在试图弄清楚新的时候(也许现在不是那么新,但对我而言是新的)C#中的任务异步编程,我遇到了一个问题,让我有点想弄清楚,我不知道为什么。

I have fixed the problem, but I am still not sure why it was a problem to begin with. I just thought I'd share my experience in case anyone out there runs into the same situation.

我已经解决了这个问题,但我仍然不确定为什么这是一个问题。我以为我会分享我的经验,以防任何人遇到同样的情况。

If any gurus would like to inform me of the cause of the problem, that'd be wonderful and much appreciated. I always like knowing just why something doesn't work!

如果有任何大师想告诉我这个问题的原因,那就太棒了,非常感激。我总是喜欢知道为什么有些东西不起作用!

I made a test task, as follows:

我做了一个测试任务,如下:

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();

And then I ran the application to see what it spat out. Here is some sample output:

然后我运行应用程序以查看它吐出的内容。这是一些示例输出:

6:06:15.5661 - Starting test task with delay of 3053ms.
6:06:15.5662 - Finished waiting.
6:06:18.5743 - Test task finished after 3063.235ms.

6:06:15.5661 - 启动延迟3053ms的测试任务。 6:06:15.5662 - 等待结束。 6:06:18.5743 - 测试任务在3063.235ms之后完成。

As you can see, the Task.WaitAll(tasks); statement didn't do much. It waited a grand total of 1 millisecond before continuing execution.

如您所见,Task.WaitAll(任务);声明没有做太多。在继续执行之前,它总共等待了1毫秒。

I have answered my own "question" below - but as I said above - if anyone more knowledgeable than I would care to explain why this doesn't work, please do! (I think it might have something to do with the execution 'stepping-out' of the method once it reaches an await operator - then stepping back in once the awaiting is done... But I am probably wrong)

我已经在下面回答了我自己的“问题” - 但正如我上面所说 - 如果有更多知识渊博的人会解释为什么这不起作用,请做! (我认为一旦它到达await运算符,它可能与该方法的执行'踩出'有关 - 然后在等待完成后退回......但我可能错了)

4 个解决方案

#1


19  

You should avoid using Task.Factory.StartNew with async-await. You should use Task.Run instead.

您应该避免将Task.Factory.StartNew与async-await一起使用。您应该使用Task.Run。

An async method returns a Task<T>, an async delegate does as well. Task.Factory.StartNew also returns a Task<T>, where its result is the result of the delegate parameter. So when used together it returns a Task<Task<T>>>.

异步方法返回Task ,异步委托也会返回。 Task.Factory.StartNew还返回一个Task ,其结果是delegate参数的结果。因此,当它们一起使用时,它返回一个Task >>。

All that this Task<Task<T>> does is execute the delegate until there's a task to return, which is when the first await is reached. If you only wait for that task to complete you aren't waiting for the whole method, just the part before the first await.

这个Task >所做的就是执行委托,直到有一个要返回的任务,即到达第一个await时。如果您只等待该任务完成,则不等待整个方法,只是等待第一个等待之前的部分。

You can fix that by using Task.Unwrap which creates a Task<T> that represents that Task<Task<T>>>:

你可以通过使用Task.Unwrap来修复它,它创建一个表示Task >>的Task :

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

#2


7  

The issue with your code is that there are two tasks at play. One is the result of your Task.Factory.StartNew call, which causes the anonymous function to be executed on the thread pool. However, your anonymous function is in turn compiled to produce a nested task, representing the completion of its asynchronous operations. When you wait on your Task<Task<object>>, you're only waiting on the outer task. To wait on the inner task, you should use Task.Run instead of Task.Factory.StartNew, since it automatically unwraps your inner task:

您的代码存在的问题是有两个任务在起作用。一个是Task.Factory.StartNew调用的结果,它导致在线程池上执行匿名函数。但是,您的匿名函数将被编译为生成嵌套任务,表示其异步操作的完成。当你等待Task >时,你只是在等待外部任务。要等待内部任务,您应该使用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


3  

Here, Task.WaitAll waits for the outer task and not the inner task. Use Task.Run to not have nested tasks. That's the best practices solution. Another solution is to also wait for the inner task. For example:

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

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

Or:

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

#4


0  

After much screwing around and hair-pulling I finally decided to get rid of the async lambda and just use the System.Threading.Thread.Sleep method, to see if that made any difference.

经过多次拧紧和拔毛后,我终于决定摆脱异步lambda,只使用System.Threading.Thread.Sleep方法,看看是否有任何区别。

The new code ended up like this:

新代码最终如下:

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();

Note: Due to removing the async keyword from the lambda method, the task type can now be simply Task<object> rather than Task<Task<object>> - you can see this change in the code above.

注意:由于从lambda方法中删除了async关键字,因此任务类型现在可以只是Task 而不是Task > - 您可以在上面的代码中看到此更改。

And, voila! It worked! I got the 'Finished waiting.' message AFTER the task completed.

瞧,瞧!有效!我得到了'等待完毕'。任务完成后的消息。

Alas, I remember reading somewhere that you shouldn't use System.Threading.Thread.Sleep() in your Task code. Can't remember why; but as it was just for testing and most tasks will actually be doing something that takes time rather than pretending to be doing something that takes time, this shouldn't really be an issue.

唉,我记得在某处你不应该在你的任务代码中使用System.Threading.Thread.Sleep()。不记得为什么;但是因为它只是用于测试而且大多数任务实际上都在做一些花费时间而不是假装做一些需要时间的事情,这不应该是一个问题。

Hopefully this helps some people out. I'm definitely not the best programmer in the world (or even close) and my code is probably not great, but if it helps someone, great! :)

希望这可以帮助一些人。我绝对不是世界上最好的程序员(甚至是关闭),我的代码可能不是很好,但如果它对某人有帮助,那就太好了! :)

Thanks for reading.

谢谢阅读。

Edit: The other answers to my question explain why I had the problem I did and this answer only solved the problem by mistake. Changing to Thread.Sleep(x) had no effect. Thank you to all the people who answered and helped me out!

编辑:我的问题的其他答案解释了为什么我遇到了问题,这个答案只是错误地解决了问题。更改为Thread.Sleep(x)无效。感谢所有回答并帮助我的人!

#1


19  

You should avoid using Task.Factory.StartNew with async-await. You should use Task.Run instead.

您应该避免将Task.Factory.StartNew与async-await一起使用。您应该使用Task.Run。

An async method returns a Task<T>, an async delegate does as well. Task.Factory.StartNew also returns a Task<T>, where its result is the result of the delegate parameter. So when used together it returns a Task<Task<T>>>.

异步方法返回Task ,异步委托也会返回。 Task.Factory.StartNew还返回一个Task ,其结果是delegate参数的结果。因此,当它们一起使用时,它返回一个Task >>。

All that this Task<Task<T>> does is execute the delegate until there's a task to return, which is when the first await is reached. If you only wait for that task to complete you aren't waiting for the whole method, just the part before the first await.

这个Task >所做的就是执行委托,直到有一个要返回的任务,即到达第一个await时。如果您只等待该任务完成,则不等待整个方法,只是等待第一个等待之前的部分。

You can fix that by using Task.Unwrap which creates a Task<T> that represents that Task<Task<T>>>:

你可以通过使用Task.Unwrap来修复它,它创建一个表示Task >>的Task :

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

#2


7  

The issue with your code is that there are two tasks at play. One is the result of your Task.Factory.StartNew call, which causes the anonymous function to be executed on the thread pool. However, your anonymous function is in turn compiled to produce a nested task, representing the completion of its asynchronous operations. When you wait on your Task<Task<object>>, you're only waiting on the outer task. To wait on the inner task, you should use Task.Run instead of Task.Factory.StartNew, since it automatically unwraps your inner task:

您的代码存在的问题是有两个任务在起作用。一个是Task.Factory.StartNew调用的结果,它导致在线程池上执行匿名函数。但是,您的匿名函数将被编译为生成嵌套任务,表示其异步操作的完成。当你等待Task >时,你只是在等待外部任务。要等待内部任务,您应该使用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


3  

Here, Task.WaitAll waits for the outer task and not the inner task. Use Task.Run to not have nested tasks. That's the best practices solution. Another solution is to also wait for the inner task. For example:

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

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

Or:

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

#4


0  

After much screwing around and hair-pulling I finally decided to get rid of the async lambda and just use the System.Threading.Thread.Sleep method, to see if that made any difference.

经过多次拧紧和拔毛后,我终于决定摆脱异步lambda,只使用System.Threading.Thread.Sleep方法,看看是否有任何区别。

The new code ended up like this:

新代码最终如下:

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();

Note: Due to removing the async keyword from the lambda method, the task type can now be simply Task<object> rather than Task<Task<object>> - you can see this change in the code above.

注意:由于从lambda方法中删除了async关键字,因此任务类型现在可以只是Task 而不是Task > - 您可以在上面的代码中看到此更改。

And, voila! It worked! I got the 'Finished waiting.' message AFTER the task completed.

瞧,瞧!有效!我得到了'等待完毕'。任务完成后的消息。

Alas, I remember reading somewhere that you shouldn't use System.Threading.Thread.Sleep() in your Task code. Can't remember why; but as it was just for testing and most tasks will actually be doing something that takes time rather than pretending to be doing something that takes time, this shouldn't really be an issue.

唉,我记得在某处你不应该在你的任务代码中使用System.Threading.Thread.Sleep()。不记得为什么;但是因为它只是用于测试而且大多数任务实际上都在做一些花费时间而不是假装做一些需要时间的事情,这不应该是一个问题。

Hopefully this helps some people out. I'm definitely not the best programmer in the world (or even close) and my code is probably not great, but if it helps someone, great! :)

希望这可以帮助一些人。我绝对不是世界上最好的程序员(甚至是关闭),我的代码可能不是很好,但如果它对某人有帮助,那就太好了! :)

Thanks for reading.

谢谢阅读。

Edit: The other answers to my question explain why I had the problem I did and this answer only solved the problem by mistake. Changing to Thread.Sleep(x) had no effect. Thank you to all the people who answered and helped me out!

编辑:我的问题的其他答案解释了为什么我遇到了问题,这个答案只是错误地解决了问题。更改为Thread.Sleep(x)无效。感谢所有回答并帮助我的人!