【问题标题】:Slow thread creation on Windows在 Windows 上创建缓慢的线程
【发布时间】:2014-11-21 09:11:28
【问题描述】:

我已使用 C++11 工具将数字运算应用程序升级为多线程程序。它在 Mac OS X 上运行良好,但不能从 Windows 上的多线程 (Visual Studio 2013) 中受益。使用以下玩具程序

#include <iostream>
#include <thread>

void t1(int& k) {
    k += 1;
};

void t2(int& k) {
    k += 1;
};

int main(int argc, const char *argv[])
{
    int a{ 0 };
    int b{ 0 };

    auto start_time = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 10000; ++i) {
        std::thread thread1{ t1, std::ref(a) };
        std::thread thread2{ t2, std::ref(b) };
        thread1.join();
        thread2.join();
    }
    auto end_time = std::chrono::high_resolution_clock::now();
    auto time_stack = std::chrono::duration_cast<std::chrono::microseconds>(
        end_time - start_time).count();
    std::cout << "Time: " << time_stack / 10000.0 << " micro seconds" <<
        std::endl;

    std::cout << a << " " << b << std::endl;

    return 0;
}

我发现在 Mac OS X 上启动一个线程需要 34 微秒,而在 Windows 上则需要 340 微秒。我在 Windows 方面做错了吗?是编译器问题吗?

【问题讨论】:

  • 我在这里运行了您的示例,rextester.com/HMWGM22211,平均得到 400 微秒。你得到类似的结果吗?
  • 算法并行不尴尬,我需要在单个 CPU 上并行化一些需要大约 500 微秒的代码。在 Mac OS X 上启动一个线程需要 30 微秒。为什么在 Windows 上也需要 300 微秒?
  • 贾格纳特:谢谢你的提示。是的,我在这个网站上也得到了大约 400 微秒。这与我在机器上的 Visual Studio 2013 上得到的相同。但在同一硬件上的 Mac OS X 和 Linux 上,它的速度要快 10 倍。
  • 尝试使用 boost::thread 看看 boost 的实现是否更快。
  • 顺便说一句,使用 clang 运行(不确定它托管在哪个平台上)rextester.com/TRXP85153 更快。

标签: windows multithreading c++11


【解决方案1】:

不是编译器问题(严格来说也不是操作系统问题)。

众所周知,创建线程是一项昂贵的操作。这尤其是在 Windows 下是正确的(在 clone 之前的 Linux 下也是如此)。
此外,创建和加入线程必然很慢,并且并不能说明创建线程本身。加入假定线程已经退出,这只能在它被安排运行后发生。因此,您的测量包括调度引入的延迟。到目前为止,您测量的时间实际上相当不错(它们可能很容易长 20 倍!)。

但是,无论如何,生成线程是否慢并不重要。

real 程序中像基准测试一样创建 20,000 个线程是一个严重错误。虽然创建数千个(甚至数百万个)线程并不是严格非法或不允许的,但使用线程的“正确”方法是创建的线程数不超过大约 CPU 内核数。也不会一直创建寿命很短的线程。
可能有一些短暂的线程,并且您可能创建一些额外的线程(例如阻塞 I/O),但您不想创建数百个或数千个。每增加一个线程(超出 CPU 内核数量)就意味着更多的上下文切换、更多的调度程序工作、更多的缓存压力,以及每个线程 1MB 的地址空间和 64kB 的物理内存(由于堆栈保留和提交粒度)。

现在,假设您在程序启动时创建了例如 10 个线程,这是否总共需要 3 毫秒并不重要。无论如何,程序启动需要几百毫秒(至少),没有人会注意到差异。

【讨论】:

  • Damon:就我而言,这是一个操作系统问题,因为 Linux 和 Windows 之间的性能比率为 10。在这里和我的真实程序中,我一次只创建 2 个线程。简而言之,我的程序执行以下操作:给定 a_0,计算 a_1 = f(a_0),然后 a_2 = f(a_1),...,f(a_(n+1)) = f(a_n)。您唯一可以并行化的是我所做的 f(x) 的计算。用一个线程计算它大约需要 500 纳秒,并且 2 个任务的并行化是显而易见的。我的想法是每次需要计算 f(x) 时创建 2 个线程。
  • 由于我有 OpenMP 背景,我认为一旦 f(x) 的计算结束,我就必须关闭它们。但有人建议我并非如此,我应该使用线程池。
  • OpenMP 应该(希望!)已经使用了一个池,而不是产生一个新的池,而是在你写 #pragma omp parallel 之类的东西时从 condvar 或类似的池中唤醒线程,然后让它们进入睡眠状态之后再次。 std::future 的合理实现应该类似地工作,但你不能保证(也无法知道)。并行化需要 500ns 的单个任务是困难的(嗯,它不是,但让它运行 比串行代码更快 几乎是不可能的)。上下文切换和缓存未命中已经抵消了所有收益,任何同步原语也是如此。
  • 从与futex 同步的上下文切换(这比产生线程快很多!)已经花费了大约 3,000 ns,由 this guy 测量。因此,为了能够真正从并行化中获得任何好处,您的任务必须至少复杂十倍,或者您必须将至少十几个任务排队。
【解决方案2】:

Visual C++ 使用并发运行时(特定于 MS)来实现 std.thread 功能。当您直接调用任何并发运行时特性/函数时,它会创建一个默认运行时对象(不详述)。或者,当您调用 std.thread 函数时,它的作用与调用 ConcRT 函数相同。

默认运行时(或者说,调度程序)的创建需要一些时间,因此它似乎需要一些时间。尝试创建一个std::thread 对象,让它运行;然后执行基准标记代码(例如上面的整个代码)。

编辑:

【讨论】:

  • Ajay:我创建了第三个线程,我在循环之前启动并在循环之后加入。但它不会改变任何时间。
  • 尝试运行发布版本。
  • 可能你没看懂我说的。尝试使用thread 对象,让它运行并处理,然后进行性能测量。或者,实际上可能是实现方式不同。
  • Ajay:我尝试过先使用线程对象,然后在循环之前处理它。但这并没有改变任何东西。我处于发布模式。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-18
  • 2017-09-04
  • 2016-09-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多