【问题标题】:Thread vs Parallel.For performance线程与并行。为了性能
【发布时间】:2018-09-04 03:09:43
【问题描述】:

我很难理解 threadsParallel.For 之间的区别。我创建了两个函数,一个使用 Parallel.For 其他调用的线程。调用 10 个线程 似乎会更快,谁能解释一下? 线程会使用系统中可用的多个处理器(以并行执行)还是仅参考 CLR 进行时间切片

public static bool ParallelProcess()
{
    Stopwatch sw = new Stopwatch();

    sw.Start();
    Parallel.For(0, 10, x =>
    {
        Console.WriteLine(string.Format("Printing {0} thread = {1}", x,
            Thread.CurrentThread.ManagedThreadId));
        Thread.Sleep(3000);
    });
    sw.Stop();
    Console.WriteLine(string.Format("Time in secs {0}", sw.Elapsed.Seconds));

    return true;
}

public static bool ParallelThread()
{
    Stopwatch sw = new Stopwatch();

    sw.Start();
    for (int i = 0; i < 10; i++)
    {
        Thread t = new Thread(new ThreadStart(Thread1));
        t.Start();
        if (i == 9)
            t.Join();
    }
    sw.Stop();
    Console.WriteLine(string.Format("Time in secs {0}", sw.Elapsed.Seconds));

    return true;
}

private static void Thread1()
{
    Console.WriteLine(string.Format("Printing {0} thread = {1}", 0,
           Thread.CurrentThread.ManagedThreadId));
    Thread.Sleep(3000);
}

当在下面的方法中调用时,Parallel.For 花费了两倍于线程的时间。

Algo.ParallelThread(); //took 3 secs
Algo.ParallelProcess();  //took 6 secs

【问题讨论】:

  • 你的ParallelThread 只等待最后一个线程完成,但不是全部。此外,您的线程不会消耗 CPU,因为它们正在休眠。 Parallel.For 未针对此类工作负载进行优化。它更适合 CPU 密集型工作。
  • 尽管 Thread 类仍然有用并且仍然有它的用途,但我会了解您当地的 tpl 库、线程池、任务。 async await 和所有现代线程架构,在你知道你需要它之​​前不要打扰 Thread
  • 你的测试有问题。
  • 尝试一个简单的实验,你需要创建 1 K 或 10 K 线程,因为许多元素需要并行处理,你就会知道为什么使用线程是个坏主意以及为什么 TPL 会摇滚

标签: c# multithreading task-parallel-library


【解决方案1】:

这里有很多地方出了问题。

(1) 不要使用sw.Elapsed.Seconds 这个值是int 并且(显然)会截断时间的小数部分。更糟糕的是,如果您有一个需要 61 秒才能完成的过程,则会报告 1,因为它就像时钟上的秒针一样。您应该改用sw.Elapsed.TotalSeconds,它报告为double,它显示总秒数,无论多少分钟或小时等。

(2) Parallel.For 使用线程池。这显着减少(甚至消除)创建线程的开销。每次调用 new Thread(() =&gt; ...) 时,您都会分配超过 1MB 的 RAM,并在进行任何处理之前消耗宝贵的资源。

(3) 您正在人为地使用 Thread.Sleep(3000); 加载线程,这意味着您掩盖了创建大量睡眠线程所需的实际时间。

(4) Parallel.For 默认情况下受 CPU 内核数量的限制。因此,当您运行 10 个线程时,工作被分成两个步骤 - 这意味着 Thread.Sleep(3000); 正在连续运行两次,因此它运行了 6 秒。 new Thread 方法是一次性运行所有线程,这意味着它只需要 3 秒多一点,但同样,Thread.Sleep(3000); 占用了线程启动时间。

(5) 您还在处理 CLR JIT 问题。第一次运行代码时,启动成本是巨大的。让我们更改代码以移除休眠并正确加入线程:

public static bool ParallelProcess()
{
    Stopwatch sw = new Stopwatch();

    sw.Start();
    Parallel.For(0, 10, x =>
    {
        Console.WriteLine(string.Format("Printing {0} thread = {1}", x, Thread.CurrentThread.ManagedThreadId));
    });
    sw.Stop();
    Console.WriteLine(string.Format("Time in secs {0}", sw.Elapsed.TotalMilliseconds));

    return true;
}

public static bool ParallelThread()
{
    Stopwatch sw = new Stopwatch();

    sw.Start();
    var threads = Enumerable.Range(0, 10).Select(x => new Thread(new ThreadStart(Thread1))).ToList();
    foreach (var thread in threads) thread.Start();
    foreach (var thread in threads) thread.Join();
    sw.Stop();

    Console.WriteLine(string.Format("Time in secs {0}", sw.Elapsed.TotalMilliseconds));

    return true;
}

private static void Thread1()
{
    Console.WriteLine(string.Format("Printing {0} thread = {1}", 0, Thread.CurrentThread.ManagedThreadId));  
}

现在,为了摆脱 CLR/JIT 启动时间,让我们像这样运行代码:

ParallelProcess();
ParallelThread();
ParallelProcess();
ParallelThread();
ParallelProcess();
ParallelThread();   

我们得到的时间是这样的:

以秒为单位的时间 3.8617 以秒为单位的时间 4.7719 以秒为单位的时间 0.3633 以秒为单位的时间 1.6332 以秒为单位的时间 0.3551 以秒为单位的时间 1.6148

与更加一致的第二次和第三次运行相比,开始运行时间很糟糕。

结果是运行Parallel.For 比调用new Thread 快4 到5 倍。

【讨论】:

  • 感谢您的详细回复。非常感激。那么, Thread.Sleep(3000) 导致在 3 秒后创建新线程,是这样吗?此外,Parallel.For 默认每次创建 3、4 个线程,这就是它花费两倍时间的原因吗?
  • @FIrePanda - Thread.Sleep(3000) NOT 导致线程在 3 秒后创建。您的代码中没有任何内容可以做到这一点。 Parallel.For 不一定会创建线程 - 如果线程池有可用的线程,它只会使用现有的线程。它花费了两倍的时间,因为它只在与可用内核一样多的线程上执行。
【解决方案2】:

Parallel 使用底层调度程序提供的线程数量,这将是线程池线程开始的最小数量。

最小线程池线程的数量默认设置为处理器的数量。随着时间的推移并基于许多不同的因素,例如当前所有线程都忙,调度程序可能会决定生成 更多 个线程并高于最小计数。

所有这些都是为您管理的,以阻止不必要的资源使用。您的第二个示例通过手动生成线程来规避所有这些。如果您明确设置线程池线程的数量,例如ThreadPool.SetMinThreads(100, 100),您会看到即使是 Parallel 也需要 3 秒,因为它立即有更多线程可供使用。

【讨论】:

  • 是的,你是对的,设置 ThreadPool.SetMinThreads(100, 100) 确实提高了性能。但是设置 ThreadPool.SetMinThreads(1, 1) 不会增加时间,为什么?
  • @FIrePanda 这就是为什么我说这是开始的最低要求。即使您降低了最小值,线程池可能仍然有更多可用线程,并且一旦不再需要它们就会逐渐淘汰它们。如果你真的想立即阻止它,你还需要设置 maximumThreadPool.SetMaxThreads(1, 1)
  • 顺便说一句,所有这些实际上只是为了解释正在发生的事情。我根本不建议搞乱这些值。如果您真的认为您可以更好地管理线程,请编写一个满足您的情况的自定义 TaskScheduler 并将其提供给您的 Parallel。
【解决方案3】:

您的 sn-ps 不等价。这是ParallelThread 的一个版本,它的作用与ParallelProcess 相同,但会启动新线程:

public static bool ParallelThread()
{
    Stopwatch sw = new Stopwatch();

    sw.Start();
    var threads = new Thread[10];
    for (int i = 0; i < 10; i++)
    {
        int x = i;
        threads[i] = new Thread(() => Thread1(x));
        threads[i].Start();
    }
    for (int i = 0; i < 10; i++)
    {
        threads[i].Join();
    }
    sw.Stop();
    Console.WriteLine(string.Format("Time in secs {0}", sw.Elapsed.Seconds));

    return true;
}

private static void Thread1(int x)
{
    Console.WriteLine(string.Format("Printing {0} thread = {1}", x,
           Thread.CurrentThread.ManagedThreadId));
    Thread.Sleep(3000);
}

在这里,我确保等待所有线程。而且,我确保匹配控制台输出。 OP 代码不做的事情。

不过,时差还是有的。

让我告诉你,至少在我的测试中,有什么不同:顺序。在ParallelThread 之前运行ParallelProcess,它们都应该需要3 秒才能完成(忽略初始运行,因为编译需要更长的时间)。我真的无法解释。

我们可以进一步修改上面的代码以使用ThreadPool,这也导致ParallelProcess 在 3 秒内完成(即使我没有修改那个版本)。这是我想出的ParallelThreadThreadPool 的版本:

public static bool ParallelThread()
{
    Stopwatch sw = new Stopwatch();

    sw.Start();
    var events = new ManualResetEvent[10];
        for (int i = 0; i < 10; i++)
    {
        int x = i;
        events[x] = new ManualResetEvent(false);
        ThreadPool.QueueUserWorkItem
            (
                _ =>
                {
                    Thread1(x);
                    events[x].Set();
                }
            );
    }
    for (int i = 0; i < 10; i++)
    {
        events[i].WaitOne();
    }
    sw.Stop();
    Console.WriteLine(string.Format("Time in secs {0}", sw.Elapsed.Seconds));

    return true;
}

private static void Thread1(int x)
{
    Console.WriteLine(string.Format("Printing {0} thread = {1}", x,
           Thread.CurrentThread.ManagedThreadId));
    Thread.Sleep(3000);
}

注意:我们可以在事件上使用WaitAll,但在STAThread 上会失败。


你有Thread.Sleep(3000),这是我们看到的 3 秒。这意味着我们并没有真正衡量任何这些方法的开销。

所以,我决定进一步研究这个问题,为此,我提高了一个数量级(从 10 到 100)并删除了 Console.WriteLine(无论如何它正在引入同步)。

这是我的代码清单:

void Main()
{
    ParallelThread();
    ParallelProcess();
}

public static bool ParallelProcess()
{
    Stopwatch sw = new Stopwatch();

    sw.Start();
    Parallel.For(0, 100, x =>
    {
        /*Console.WriteLine(string.Format("Printing {0} thread = {1}", x,
            Thread.CurrentThread.ManagedThreadId));*/
        Thread.Sleep(3000);
    });
    sw.Stop();
    Console.WriteLine(string.Format("Time in secs {0}", sw.Elapsed.Seconds));

    return true;
}

public static bool ParallelThread()
{
    Stopwatch sw = new Stopwatch();

    sw.Start();
    var events = new ManualResetEvent[100];
        for (int i = 0; i < 100; i++)
    {
        int x = i;
        events[x] = new ManualResetEvent(false);
        ThreadPool.QueueUserWorkItem
            (
                _ =>
                {
                    Thread1(x);
                    events[x].Set();
                }
            );
    }
    for (int i = 0; i < 100; i++)
    {
        events[i].WaitOne();
    }
    sw.Stop();
    Console.WriteLine(string.Format("Time in secs {0}", sw.Elapsed.Seconds));

    return true;
}

private static void Thread1(int x)
{
    /*Console.WriteLine(string.Format("Printing {0} thread = {1}", x,
           Thread.CurrentThread.ManagedThreadId));*/
    Thread.Sleep(3000);
}

ParallelThread 得到 6 秒,ParallelProcess 得到 9 秒。即使在颠倒顺序之后也是如此。这让我更加确信这是对开销的真实衡量。

添加ThreadPool.SetMinThreads(100, 100); 可将ParallelThread(请记住,此版本使用ThreadPool)和ParallelProcess 的时间缩短至3 秒。这意味着这个开销来自线程池。现在,我可以回到产生新线程的版本(修改为产生 100 并带有 Console.WriteLine 注释):

public static bool ParallelThread()
{
    Stopwatch sw = new Stopwatch();

    sw.Start();
    var threads = new Thread[100];
    for (int i = 0; i < 100; i++)
    {
        int x = i;
        threads[i] = new Thread(() => Thread1(x));
        threads[i].Start();
    }
    for (int i = 0; i < 100; i++)
    {
        threads[i].Join();
    }
    sw.Stop();
    Console.WriteLine(string.Format("Time in secs {0}", sw.Elapsed.Seconds));

    return true;
}

private static void Thread1(int x)
{
    /*Console.WriteLine(string.Format("Printing {0} thread = {1}", x,
           Thread.CurrentThread.ManagedThreadId));*/
    Thread.Sleep(3000);
}

我从这个版本中得到一致的 3 秒(这意味着时间开销可以忽略不计,因为正如我之前所说,Thread.Sleep(3000) 是 3 秒),但是我想指出,它会留下更多的垃圾来收集而不是使用ThreadPoolParallel.For。另一方面,使用Parallel.For 仍然与ThreadPool 绑定。顺便说一句,如果你想降低它的性能,减少最小线程数是不够的,你还得降低最大线程数(例如ThreadPool.SetMaxThreads(1, 1);)。

总而言之,请注意Parallel.For 更容易使用,更难出错。


调用 10 个线程似乎会更快,谁能解释一下?

产生线程很快。虽然,它会导致更多的垃圾。另外,请注意您的测试不是很好。

线程会使用系统中可用的多个处理器(以并行执行)还是仅参考 CLR 进行时间切片?

是的,他们会的。它们映射到底层操作系统线程,可以被它抢占,并将根据它们的亲和力在任何内核中运行(参见ProcessThread.ProcessorAffinity)。需要明确的是,它们不是fibers,也不是协程。

【讨论】:

    【解决方案4】:

    用最简单的术语来说,使用Thread 类可以保证在操作系统级别创建线程,但使用Parallel.For,CLR 在生成操作系统级别线程之前会三思而后行。如果觉得现在是在 OS 级别创建线程的好时机,则继续,否则使用可用的线程池。 TPL 被编写为针对多核环境进行优化。

    【讨论】:

    • 我想知道为什么投反对票!是因为我在原始答案中错误地跳过了 ThreadPool 吗?
    猜你喜欢
    • 2015-03-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-10
    相关资源
    最近更新 更多