异步方法未并行运行

4
在下面的代码中,在B方法中,代码Trace.TraceInformation("B - Started");从未被调用。
这个方法应该并行运行吗?
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        private static async Task A()
        {
            for (;;)
            {
            }
        }


        private static async Task B()
        {
            Trace.TraceInformation("B - Started");
        }

        static void Main(string[] args)
        {
            var tasks = new List<Task> { A(), B() };
            Task.WaitAll(tasks.ToArray());
        }
    }
}

也许这是繁忙等待的一种特性。A()会将CPU占用率提高到100%吗? - MatthewMartin
我有4个处理器。其中一个占用100%。不过,它仍然应该同时运行(并行)吗? - Fernando Silva
1
@FernandoSilva 请查看 Task.Run。仅添加 async 并不能使它们并行。 - Eser
1
@FernandoSilva:这段代码会导致编译器警告。请阅读它们,它们会准确地解释发生了什么。 - Stephen Cleary
3个回答

11

简短回答

不,按照你编写的两个async方法,它们确实没有并行运行。在第一个方法中(例如循环内部),添加await Task.Yield();将使它们能够并行运行,但是根据您实际需要的情况(单线程交替执行?多线程实际并行执行?)有更合理和直接的方法。

详细回答

首先,将函数声明为async本质上并不会使它们异步运行或者做什么。它只是简化语法,以便使用异步方式 - 在此处了解更多概念:使用Async和Await的异步编程

事实上,A根本不是异步的,因为其方法体中没有一个await。第一次使用await之前的指令像常规方法一样同步运行。

从那时起,您await的对象确定接下来会发生什么,即剩余方法运行的上下文。

强制在另一个线程上运行任务,请使用Task.Run或类似的方法。

在这种情况下,添加await Task.Yield()就可以了,因为当前同步上下文为null,这确实会导致任务调度程序(应该是ThreadPoolTaskScheduler)在线程池线程上执行剩余指令 - 某些环境或配置可能会导致您只有一个线程,因此事情仍然不会并行运行。

总结

故事的寓意是:要了解以下两个概念之间的区别:

  • 并发(通过合理使用async/await实现)和
  • 并行性(仅在同时进行的任务得到正确调度时发生 或者 如果您使用Task.RunThread等来强制执行它,在这种情况下,async的使用完全无关紧要)

2
async关键字并不是一个神奇的开启线程的标记。它的唯一目的是让编译器知道方法可能依赖于某些异步操作(复杂的数据处理线程,I/O...),因此必须设置状态机来协调这些异步操作产生的回调函数。
要使A在另一个线程上运行,您需要使用Task.Run来调用它,该调用会将其封装在一个新线程上,并返回一个Task对象,您可以使用await来等待该对象的返回值。需要注意的是,使用await等待方法并不意味着您的代码将与A的执行自身并行:直到您await Task对象的那一行代码之前,它都会运行,告诉编译器您需要Task对象返回的值。在这种情况下,await Task.Run(A)将有效地使您的程序永远运行下去,等待A返回,除非计算机出现故障,否则永远不会发生。
请注意,将方法标记为async但实际上未等待任何内容只会导致编译器警告的效果。如果您等待的内容不是真正的异步操作(它立即使用类似Task.FromResult返回调用线程),那么这意味着您的程序会出现运行速度惩罚,但非常微小。

1
不,所展示的方法并不会“同时运行”。 为什么B从未被调用——你有一个由一系列Add调用构建而成的任务列表tasks,并且首先添加的是A()的结果。由于A方法没有任何等待,它将在同一线程上同步完成。之后将调用B()。 现在A永远不会完成(它正处于无限循环中),所以代码实际上甚至不会到达B的调用。 请注意,即使创建成功,代码也永远不会完成WaitAll,因为A仍然处于无限循环中。 如果要让方法“并行运行”,则需要在新线程上隐式/显式地运行它们(即使用Task.Run或Thread.Start),或者对于I/O绑定调用,让方法通过await释放线程。

2
这是正确的,但它是一个非常不完整的答案。 - Machinarius
你不应该删除它,因为它以当前的形式添加了非常重要的信息。 - Machinarius

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