我正在玩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的代码慢?
答案 0 :(得分:15)
你有两个线程争夺同一个变量k
。因此,你花时间在处理器上说“处理器1:嘿,你知道k
有什么价值吗?处理器2:当然,你走了!”,每次更新都来回乒乓。由于k
不是原子的,因此也无法保证thread2不会写入k
的“旧”值,以便下次线程1读取该值时,它会跳回1,2,10或100步,并且必须重新做 - 理论上可能导致每次完成都没有循环,但这将需要相当多的坏运气。
答案 1 :(得分: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为每个线程在外部作用域提供自己的存储,并在连接后汇总结果,从而无需使用互斥锁。我将此作为练习留给读者。
答案 2 :(得分:2)
对我而言,似乎更重要的问题是“为什么这不起作用?”是“我如何让这个工作?”对于手头的任务,我认为std::async
(尽管significant shortcomings)确实是比直接使用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;
}
答案 3 :(得分: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
我忽略大秒的小数秒。我的代码只更新了一个共享变量 oriencnt 。我还没有让它更新 fsq 。它看起来在线程版本中,系统正在做更多的工作,这导致更长的时钟时间(实时)。我的编译器标志是默认的-g -O2,不确定这是否是关键问题。使用-O3编译时,差异很小。还有一些互斥锁控制的IO操作。我的实验表明,这并没有造成差异。我正在使用gcc 5.4和C ++ 11。一种可能性是库未优化。
这是用O3编译的
Thead No_thread
=========================
real 4m24 2m44s
usr 0m54s 2m44s
sys 0m6.2s 0m0.016s