CPU 基准测试:Tasks vs ThreadPool vs Thread

Ted*_*Ted -1 c# multithreading task async-await

我在这里发布了另一个 SO 问题,作为后续,我的同事做了一个测试,如下所示,作为 async/await/Tasks 参数的某种形式的“反击”。

(我知道lock不需要 on resultList,请忽略)

  • 我知道 async/await 和 Tasks不是用来处理 CPU 密集型任务,而是用来处理由操作系统完成的 I/O 操作。下面的基准测试是一项 CPU 密集型任务,因此测试从一开始就有缺陷。
  • 但是,据我了解, usingnew Task().Start()会在 ThreadPool 上调度操作,并在 ThreadPool 上的不同线程上执行测试代码。这是否意味着第一次和第二次测试或多或少相同?(我猜不是,请解释
  • 那为什么他们之间的差别这么大呢?

在此处输入图片说明

Ste*_*ary 5

async/await/Tasks 参数的某种形式的“计数器”。

发布的代码与asyncor完全无关await。它比较了三种不同的并行性:

  1. 动态任务并行。
  2. 直接线程池访问。
  3. 手动多线程与手动分区。

前两者有一定的可比性。当然,直接线程池访问会比动态任务并行更快。但是这些测试没有表明直接线程池访问更难正确执行。特别是,当您运行实际代码并需要处理异常返回值时,您必须将样板代码和对象实例添加到直接线程池访问代码中,这会减慢它的速度。

第三个完全没有可比性。它仅使用 10 个手动线程。同样,这个例子忽略了现实世界代码中必要的额外复杂性;具体来说,需要处理异常和返回值。它还假定分区大小,这是有问题的;现实世界的代码没有那么奢侈。如果您正在管理自己的线程集,那么您必须决定诸如当队列有很多项目时应该多快增加线程数,以及当队列为空时应该多快结束线程等事情。在您真正比较同一事物之前,这些都是困难的问题,它们会在 #3 测试中添加大量代码。

更不用说维护成本了。根据我的经验(即作为应用程序开发人员),微优化是不值得的。即使您采用“最差”(#1)方法,每个项目也会损失大约 7微秒。这是一个难以想象的小额储蓄。作为一般规则,开发人员时间对您的公司来说比用户时间更有价值。如果您的用户必须处理十万个项目,则几乎无法察觉差异。如果您采用“最佳”(#3)方法,则代码的可维护性将大大降低,特别是考虑到生产代码中所需的样板和线程管理代码,此处未显示。只需编写或阅读代码,就可以节省用户时间。

哦,所有这一切中最有趣的部分是,将所有这些不同类型的并行性进行比较,它们甚至没有包括最适合此测试的一种:PLINQ。

static void Main(string[] args)
{
    TaskParallelLibrary();
    ManualThreads();
    Console.ReadKey();
}

static void ManualThreads()
{
    var queue = new List<string>();
    for (int i = 0; i != 1000000; ++i)
        queue.Add("string" + i);
    var resultList = new List<string>();
    var stopwatch = Stopwatch.StartNew();
    var counter = 0;
    for (int i = 0; i != 10; ++i)
    {
        new Thread(() =>
        {
            while (true)
            {
                var t = "";
                lock (queue)
                {
                    if (counter >= queue.Count)
                        break;
                    t = queue[counter];
                    ++counter;
                }
                t = t.Substring(0, 5);
                string t2 = t.Substring(0, 2) + t;
                lock (resultList)
                    resultList.Add(t2);
            }
        }).Start();
    }
    while (resultList.Count < queue.Count)
        Thread.Sleep(1);
    stopwatch.Stop();
    Console.WriteLine($"Manual threads: Processed {resultList.Count} in {stopwatch.Elapsed}");
}

static void TaskParallelLibrary()
{
    var queue = new List<string>();
    for (int i = 0; i != 1000000; ++i)
        queue.Add("string" + i);
    var stopwatch = Stopwatch.StartNew();
    var resultList = queue.AsParallel().Select(t =>
    {
        t = t.Substring(0, 5);
        return t.Substring(0, 2) + t;
    }).ToList();
    stopwatch.Stop();
    Console.WriteLine($"Parallel: Processed {resultList.Count} in {stopwatch.Elapsed}");
}
Run Code Online (Sandbox Code Playgroud)

在我的机器上,多次运行此代码后,我发现 PLINQ 代码的性能比手动线程高出约 30%。.NET Core 3.0 preview5-27626-15 上的示例输出,专为 Release 构建,独立运行:

Parallel: Processed 1000000 in 00:00:00.3629408
Manual threads: Processed 1000000 in 00:00:00.5119985
Run Code Online (Sandbox Code Playgroud)

当然,PLINQ 代码是:

  • 较短
  • 更易于维护
  • 更健壮(处理异常和返回类型)
  • 不那么尴尬(无需轮询完成)
  • 更便携(基于处理器数量的分区)
  • 更灵活(根据工作量自动调整线程池)