【问题标题】:PLINQ vs Sync Over Async: what is the difference?PLINQ vs Sync Over Async:有什么区别?
【发布时间】:2022-02-07 17:05:36
【问题描述】:

我喜欢 PLINQ。在我所有关于各种并行化模式的测试用例中,它都表现良好且始终如一。但是我最近遇到了一个让我有点困扰的问题:这两个示例之间的功能区别是什么?什么(如果有的话)使 PLINQ 示例与以下反模式不相似或不等价?

PLINQ:

public int PLINQSum()
{
    return Enumerable.Range(0, N)
        .AsParallel()
        .Select((x) => x + 1)
        .Sum();
}

通过异步同步:

public int AsyncSum()
{
    var tasks = Enumerable.Range(0, N)
        .Select((x) => Task.Run(() => x + 1));

    return Task.WhenAll(tasks).Result.Sum();
}

【问题讨论】:

  • 一个观察到的行为差异:在我的测试中,其中 N 是要完成的操作数,M 是操作的大小,随着 N 的增加和 M 的减少,PLINQ 保持同步或低于同步运行时,而上面的异步示例比同步测试慢了几个数量级。
  • this answer。它讨论了foreachParallel.ForEachTask.Run 之间的区别,但这同样适用于PLINQ。
  • 您的情况的另一个区别是,一旦您将Task.Run 嵌套在Select 中,您就会使用Task<int> 对象而不是int 值,因此您不能轻易链接其他 PLINQ 操作(例如 Where),而无需等待这些任务完成。

标签: c# async-await parallel-processing plinq


【解决方案1】:

AsyncSum 方法不是Sync over Async 的示例。这是使用Task.Run 方法以并行计算的示例。您可能认为Task = async,但事实并非如此。 Task 类是在 2010 年随 .NET Framework 4.0 引入的,作为 Task Parallel Librarytwo years before 的一部分,是 2012 年随 .NET Framework 4.5 出现的异步/等待技术的一部分。

什么是异步同步:我们使用这个术语来描述异步 API 被调用然后同步等待,导致线程被阻塞直到异步操作完成的情况。这意味着异步 API 具有真正的异步实现,这意味着它在操作进行中时使用no thread。 .NET 平台内置的大多数异步 API,but not all,都具有真正的异步实现。

您问题中的两个示例在技术上是不同的,但这并不是因为其中一个是 Sync over Async。他们都不是。两者都在并行化同步操作(数学加法x + 1),如果不使用 CPU 就无法执行。当我们使用 CPU 时,我们使用线程。

AsyncSum 方法描述为反模式可能是公平的,但不是因为它是同步而不是异步。您可能希望将其称为反模式,因为:

  1. 它为序列中的每个数字分配和安排Task,与必须执行的微小计算工作相比,会产生巨大的开销。
  2. 它在并行操作的整个持续时间内使ThreadPool 饱和。
  3. 它强制ThreadPool 创建额外的线程,导致超额订阅(线程多于 CPU)。这会导致操作系统有更多工作要做(在线程之间切换)。
  4. 在出现异常时会出现不良行为。它不会在发生错误后立即停止操作,而是始终为序列中的所有元素调用 lambda。因此,您必须等待更长的时间才能观察到错误,最后您可能会观察到大量错误。
  5. 它不使用当前线程。当前线程被阻塞,什么都不做,而所有工作都由ThreadPool线程完成。相比之下,PLINQ 将当前线程用作其工作线程之一。您也可以手动执行此操作,通过使用Task 构造函数(而不是Task.Run)创建一些任务,然后使用RunSynchronously 方法在当前线程上运行它们,而其余任务安排在ThreadPool
var task1 = new Task<int>(() => 1 + 1); // Cold task
var task2 = Task.Run(() => 2 + 2); // Hot task scheduled on the ThreadPool
task1.RunSynchronously(); // Run the cold task on the current thread
int sum = Task.WhenAll(task1, task2).Result.Sum(); // Wait both tasks

名称AsyncSum 本身是不合适的,因为该方法内部没有任何异步发生。一个更好的名字可能是WhenAll_TaskRun_Sum

【讨论】:

  • 虽然我同意您在这里的一些评估,但根据关于 Task.Run 的 Microsoft 文档,传递给该方法的操作是“异步”执行的操作,没有“异步”关键字, sync over async 反模式的最佳定义是“当您在异步方法上使用阻塞等待,而不是异步等待结果时。这会浪费线程,导致无响应(如果从 UI 调用),并且使您面临潜在的僵局。” makolyte.com/fixing-the-sync-over-async-antipattern
  • 虽然,您是对的,PLINQ 显然是通过底层优化重用了原始线程,而在后一个示例中并没有发生这种情况。
  • @RTD 你说得对,文档将Task.Run action 参数描述为“异步执行的工作”。但是,如果您查看 Thread 构造函数的文档,它会说:start:“表示该线程开始执行时要调用的方法的委托”。我的观点是,文档中选择性地使用了术语“异步”,即使是用于执行类似操作的 API。 Task.Run(action)new Thread(action).Start() 都调用另一个线程上的操作。你会说启动一个线程是一个异步操作吗?还是说它是一种反模式?
  • 我的理解是“异步”和“并行”之间存在一些重叠,但它们并不等同,因为异步任务可能会或可能不会被调度到原始线程,具体取决于任务调度程序,如果它是来自线程池的后台线程。在提供的示例中可能并非如此,它可以是控制台应用程序中的前台线程,但如果由于某种不幸的原因它是 ASP.Net 为请求提供服务,它将来自线程池,所以它变成通过异步同步,阻止工作线程重用。
猜你喜欢
  • 2011-05-23
  • 1970-01-01
  • 1970-01-01
  • 2013-08-11
  • 1970-01-01
  • 2019-08-19
  • 2012-08-17
相关资源
最近更新 更多