CPU基准测试:任务vs线程池vs线程

时间:2019-06-04 07:35:56

标签: c# multithreading async-await task

我发布了another SO question here,作为后续工作,我的同事进行了测试,如下所示,它是对async / await / Tasks参数的某种形式的“计数”。

(我知道不需要resultList上的lock,请忽略它)

  • 我知道async / await和Tasks不是用来处理CPU密集型任务的,而是用来处理OS进行的I / O操作的。下面的基准测试是一项占用大量CPU的任务,因此该测试从一开始就是有缺陷的。
  • 但是,据我了解,使用new Task().Start()将在ThreadPool上安排操作并在ThreadPool上的不同线程上执行测试代码。这是否意味着第一项和第二项测试大致相同? (我猜不是,请解释
  • 为什么它们之间有很大的区别?

enter image description here

1 个答案:

答案 0 :(得分:4)

  

某种形式的“计数器”,用于异步/等待/任务的参数。

发布的代码与asyncawait绝对无关。它在比较三种不同的并行性:

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

前两个有点可比。当然,直接线程池访问将比动态任务并行性更快。但是这些测试没有显示出直接线程池访问要正确地进行要困难得多。特别是,当您运行真实世界的代码并需要处理 exceptions return values 时,必须将样板代码和对象实例添加到直接线程池访问代码中减慢了速度。

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

这甚至没有说维护成本。以我的经验(即作为应用程序开发人员),微优化是不值得的。即使您采用“最差”(#1)方法,每个项目也会损失大约7个微秒。这真是少得可怜的节省。通常,开发人员时间对您公司的价值远比用户时间宝贵。如果您的用户必须处理十万个项目,那么差异几乎是无法察觉的。如果您采用“最佳”(#3)方法,则该代码的可维护性将大大降低,尤其是考虑到生产代码中必需的样板代码和线程管理代码,此处未显示。与#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}");
}

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

Parallel: Processed 1000000 in 00:00:00.3629408
Manual threads: Processed 1000000 in 00:00:00.5119985

当然,PLINQ代码是:

  • 缩手
  • 更易于维护
  • 更强大(处理异常和返回类型)
  • 少尴尬(无需轮询即可完成)
  • 更便携(根据处理器数量进行分区)
  • 更灵活(根据工作量自动调整线程池)