7

我在玩,std::thread突然出现了一些奇怪的东西:

#include <thread>

int k = 0;

int main() {
    std::thread t1([]() { while (k < 1000000000) { k = k + 1; }});
    std::thread t2([]() { while (k < 1000000000) { k = k + 1; }});

    t1.join();
    t2.join();

    return 0;
}

当使用 clang++ 编译上述代码时,我得到了以下基准:

real 0m2.377s  
user 0m4.688s  
sys  0m0.005s

然后我将代码更改为以下内容:(现在仅使用 1 个线程)

#include <thread>

int k = 0;

int main() {
    std::thread t1([]() { while (k < 1000000000) { k = k + 1; }});

    t1.join();

    return 0;
}

这些是新的基准:

real 0m2.304s
user 0m2.298s
sys  0m0.003s

为什么使用 2 个线程的代码比使用 1 个线程的代码慢?

4

4 回答 4

16

您有两个线程争夺同一个变量,k. 所以你花时间在处理器说“处理器 1:嘿,你知道有什么价值k吗?处理器 2:当然,给你!”,每隔几次更新就来回打乒乓球。由于k不是原子的,因此也不能保证 thread2 不会写入“旧”值,k因此下次线程 1 读取该值时,它会跳回 1、2、10 或 100 步,并且必须重新执行再次 - 从理论上讲,每次完成都不会导致任何循环,但这需要相当多的运气。

于 2013-06-16T20:01:16.970 回答
4

这确实应该是对 Mats Petersson 的回答的评论,但我想提供代码示例。

问题是特定资源的争用以及缓存行。

备选方案 1:

#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>

static const uint64_t ITERATIONS = 10000000000ULL;

int main(int argc, const char** argv)
{
    size_t numThreads = 1;
    if (argc > 1) {
        numThreads = strtoul(argv[1], NULL, 10);
        if (numThreads == 0)
            return -1;
    }

    std::vector<std::thread> threads;

    uint64_t k = 0;
    for (size_t t = 0; t < numThreads; ++t) {
       threads.emplace_back([&k]() { // capture k by reference so we all use the same k.
           while (k < ITERATIONS) {
               k++;
           }
       });
    }

    for (size_t t = 0; t < numThreads; ++t) {
        threads[t].join();
    }
    return 0;
}

在这里,线程争用一个变量,同时执行读取和写入,这迫使它乒乓球导致争用并使单线程情况最有效。

#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>
#include <atomic>

static const uint64_t ITERATIONS = 10000000000ULL;

int main(int argc, const char** argv)
{
    size_t numThreads = 1;
    if (argc > 1) {
        numThreads = strtoul(argv[1], NULL, 10);
        if (numThreads == 0)
            return -1;
    }

    std::vector<std::thread> threads;

    std::atomic<uint64_t> k = 0;
    for (size_t t = 0; t < numThreads; ++t) {
       threads.emplace_back([&]() {
           // Imperfect division of labor, we'll fall short in some cases.
           for (size_t i = 0; i < ITERATIONS / numThreads; ++i) {
               k++;
           }
       });
    }

    for (size_t t = 0; t < numThreads; ++t) {
        threads[t].join();
    }
    return 0;
}

在这里,我们确定性地分工(我们遇到了 numThreads 不是 ITERATIONS 的除数但对于这个演示来说足够接近的情况)。不幸的是,我们仍然遇到访问内存中共享元素的争用。

#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>
#include <atomic>

static const uint64_t ITERATIONS = 10000000000ULL;

int main(int argc, const char** argv)
{
    size_t numThreads = 1;
    if (argc > 1) {
        numThreads = strtoul(argv[1], NULL, 10);
        if (numThreads == 0)
            return -1;
    }

    std::vector<std::thread> threads;
    std::vector<uint64_t> ks;

    for (size_t t = 0; t < numThreads; ++t) {
       threads.emplace_back([=, &ks]() {
           auto& k = ks[t];
           // Imperfect division of labor, we'll fall short in some cases.
           for (size_t i = 0; i < ITERATIONS / numThreads; ++i) {
               k++;
           }
       });
    }

    uint64_t k = 0;
    for (size_t t = 0; t < numThreads; ++t) {
        threads[t].join();
        k += ks[t];
    }
    return 0;
}

同样,这对于工作负载的分配是确定性的,最后我们会花费少量精力来整理结果。然而,我们没有采取任何措施来确保计数器的分布有利于 CPU 的健康分布。为了那个原因:

#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>

static const uint64_t ITERATIONS = 10000000000ULL;
#define CACHE_LINE_SIZE 128

int main(int argc, const char** argv)
{
    size_t numThreads = 1;
    if (argc > 1) {
        numThreads = strtoul(argv[1], NULL, 10);
        if (numThreads == 0)
            return -1;
    }

    std::vector<std::thread> threads;
    std::mutex kMutex;
    uint64_t k = 0;

    for (size_t t = 0; t < numThreads; ++t) {
       threads.emplace_back([=, &k]() {
           alignas(CACHE_LINE_SIZE) uint64_t myK = 0;
           // Imperfect division of labor, we'll fall short in some cases.
           for (uint64_t i = 0; i < ITERATIONS / numThreads; ++i) {
               myK++;
           }
           kMutex.lock();
           k += myK;
           kMutex.unlock();
       });
    }

    for (size_t t = 0; t < numThreads; ++t) {
        threads[t].join();
    }
    return 0;
}

在这里,我们避免线程之间的争用一直到缓存行级别,除了最后我们使用互斥锁来控制同步的单一情况。对于这种微不足道的工作量,互斥锁将具有非常高的相对成本。或者,您可以使用 alignas 在外部范围内为每个线程提供其自己的存储,并在连接后汇总结果,从而消除对互斥体的需要。我把它作为练习留给读者。

于 2013-06-16T22:09:44.093 回答
2

在我看来,比“为什么这不起作用?”更重要的问题是。是“我怎样才能让它发挥作用?” 对于手头的任务,我认为std::async(尽管有很大的缺点)确实是比std::thread直接使用更好的工具。

#include <future>
#include <iostream>

int k = 0;
unsigned tasks = std::thread::hardware_concurrency();
unsigned reps = 1000000000 / tasks;

int main() {
    std::vector<std::future<int>> f;

    for (int i=0; i<tasks; i++)
        f.emplace_back(std::async(std::launch::async, 
                                  [](){int j; for (j=0; j<reps; j++); return j;})
                      );

    for (int i=0; i<tasks; i++) {
        f[i].wait();
        k += f[i].get();
    }

    std::cout << k << "\n";
    return 0;
}
于 2013-06-16T22:56:02.003 回答
0

我遇到了这个问题。我的观点是,对于某些类型的工作,管理线程的成本可能会超过在线程中运行所获得的收益。这是我的代码示例,在循环大量迭代中做一些实际工作,所以我得到了与 time 命令非常一致的数字。

   pair<int,int> result{0,0};
#ifdef USETHREAD
      thread thread_l(&Myclass::trimLeft, this, std::ref(fsq), std::ref(oriencnt), std::ref(result.first));
      thread thread_r(&Myclass::trimRight, this, std::ref(fsq), std::ref(oriencnt), std::ref(result.second));
      thread_l.join();
      thread_r.join();
#else
      // non threaded version faster
      trimLeft(fsq, oriencnt, result.first);
      trimRight(fsq, oriencnt, result.second);
#endif

   return result;

时间结果

Thead          No_thread
===========================    
Real  4m28s          2m49s
usr   0m55s          2m49s
sys   0m6.2s         0m0.012s

对于大的,我忽略了小数秒。我的代码只更新一个共享变量orientcnt。我还没有让它更新fsq。看起来在线程版本中,系统正在做更多的工作,从而导致更长的时钟时间(实时)。我的编译器标志是默认的 -g -O2,不确定这是否是关键问题。使用 -O3 编译时,差异很小。还有一些互斥控制的 IO 操作。我的实验表明,这不会导致差异。我正在使用带有 C++11 的 gcc 5.4。一种可能性是该库未优化。

这里是用O3编译的

       Thead    No_thread
=========================
real   4m24        2m44s
usr    0m54s       2m44s
sys    0m6.2s      0m0.016s
于 2017-07-02T01:30:37.767 回答