我试图通过线程类写出How to get 100% CPU usage from a C program问题的答案。这是我的代码
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
using namespace std;
static int primes = 0;
void prime(int a, int b);
mutex mtx;
int main()
{
unsigned int nthreads = thread::hardware_concurrency();
vector<thread> threads;
int limit = 1000000;
int intrvl = (int) limit / nthreads;
for (int i = 0; i < nthreads; i++)
{
threads.emplace_back(prime, i*intrvl+1, i*intrvl+intrvl);
}
cout << "Number of logical cores: " << nthreads << "\n";
cout << "Calculating number of primes less than " << limit << "... \n";
for (thread & t : threads) {
t.join();
}
cout << "There are " << primes << " prime numbers less than " << limit << ".\n";
return 0;
}
void prime(int a, int b)
{
for (a; a <= b; a++) {
int i = 2;
while(i <= a) {
if(a % i == 0)
break;
i++;
}
if(i == a) {
mtx.lock();
primes++;
mtx.unlock();
}
}
}
但是当我运行它时,我得到以下图表
那是正弦曲线。但是当我运行使用openmp的@Mysticial回答时,我得到了这个
我通过ps -eLf
检查了两个程序,并且它们都使用了8个线程。为什么我得到这个不稳定的图表?如何获得与openmp对线程相同的结果?
答案 0 :(得分:7)
Mystical's answer与您的代码之间存在一些根本区别。
您的代码为每个CPU创建了一大块工作,并让它运行完成。这意味着一旦线程完成,CPU使用率将急剧下降,因为CPU将处于空闲状态,而其他线程将运行完成。这是因为调度并不总是公平的。一个线程可能会比其他线程更快地完成并完成。
OpenMP解决方案通过声明schedule(dynamic)
来解决这个问题,它告诉OpenMP在内部创建一个所有线程将使用的工作队列。当一大块工作完成后,在代码中退出的线程将消耗另一部分工作并忙于工作。
最终,这成为挑选足够大小的块的平衡行为。太大,并且在任务结束时CPU可能不会超出。太小了,可能会有很大的开销。
您正在写入在所有线程之间共享的变量primes
。
这有两个后果:
OpenMP解决方案通过reducing通过operator+()
解决了这个问题,每个线程保存到最终结果中的primes
个体值的结果。这就是reduction(+ : primes)
的作用。
通过了解OpenMP如何分割,安排工作以及组合结果,我们可以修改您的代码,使其行为相似。
#include <iostream>
#include <thread>
#include <vector>
#include <utility>
#include <algorithm>
#include <functional>
#include <mutex>
#include <future>
using namespace std;
int prime(int a, int b)
{
int primes = 0;
for (a; a <= b; a++) {
int i = 2;
while (i <= a) {
if (a % i == 0)
break;
i++;
}
if (i == a) {
primes++;
}
}
return primes;
}
int workConsumingPrime(vector<pair<int, int>>& workQueue, mutex& workMutex)
{
int primes = 0;
unique_lock<mutex> workLock(workMutex);
while (!workQueue.empty()) {
pair<int, int> work = workQueue.back();
workQueue.pop_back();
workLock.unlock(); //< Don't hold the mutex while we do our work.
primes += prime(work.first, work.second);
workLock.lock();
}
return primes;
}
int main()
{
int nthreads = thread::hardware_concurrency();
int limit = 1000000;
// A place to put work to be consumed, and a synchronisation object to protect it.
vector<pair<int, int>> workQueue;
mutex workMutex;
// Put all of the ranges into a queue for the threads to consume.
int chunkSize = max(limit / (nthreads*16), 10); //< Handwaving came picking 16 and a good factor.
for (int i = 0; i < limit; i += chunkSize) {
workQueue.push_back(make_pair(i, min(limit, i + chunkSize)));
}
// Start the threads.
vector<future<int>> futures;
for (int i = 0; i < nthreads; ++i) {
packaged_task<int()> task(bind(workConsumingPrime, ref(workQueue), ref(workMutex)));
futures.push_back(task.get_future());
thread(move(task)).detach();
}
cout << "Number of logical cores: " << nthreads << "\n";
cout << "Calculating number of primes less than " << limit << "... \n";
// Sum up all the results.
int primes = 0;
for (future<int>& f : futures) {
primes += f.get();
}
cout << "There are " << primes << " prime numbers less than " << limit << ".\n";
}
这仍然不能完美再现OpenMP示例的行为方式。例如,这更接近OpenMP的static
计划,因为工作块是固定大小的。此外,OpenMP根本不使用工作队列。所以我可能会撒谎一点 - 称之为白色谎言,因为我想更清楚地表明工作被分开了。在幕后可能做的是存储下一个线程在可用时应该开始的迭代以及下一个块大小的启发式。
即使存在这些差异,我也可以在很长一段时间内最大限度地利用所有CPU。
您可能已经注意到OpenMP版本更具可读性。这是因为它意味着像这样解决问题。因此,当我们尝试在没有库或编译器扩展的情况下解决它们时,我们最终会重新发明轮子。幸运的是,要将这种功能直接引入C ++,还有很多工作要做。具体来说,Parallelism TS
可以帮助我们,如果我们可以将其表示为标准C ++算法。然后我们可以告诉库在所有CPU中分配算法,因为它适合我们。
在C ++ 11中,借助Boost的一些帮助,这个算法可以写成:
#include <iostream>
#include <iterator>
#include <algorithm>
#include <boost/range/irange.hpp>
using namespace std;
bool isPrime(int n)
{
if (n < 2)
return false;
for (int i = 2; i < n; ++i) {
if (n % i == 0)
return false;
}
return true;
}
int main()
{
auto range = boost::irange(0, 1000001);
auto numPrimes = count_if(begin(range), end(range), isPrime);
cout << "There are " << numPrimes << " prime numbers less than " << range.back() << ".\n";
}
要对算法进行并行处理,您只需要#include <execution_policy>
并将std::par
作为count_if
的第一个参数传递。
auto numPrimes = count_if(par, begin(range), end(range), isPrime);
这就是让我乐于阅读的代码。
注意:绝对没有时间花在优化此算法上。如果我们要进行任何优化,我会研究Sieve of Eratosthenes之类的东西,它使用以前的主要计算来帮助未来的。
答案 1 :(得分:4)
首先,您需要意识到OpenMP通常在封面下有一个相当复杂的线程池,所以匹配它(确切地说)可能至少有些困难。
其次,在我看来,在优化线程之前, 应该尝试以至少一半的正常基本算法开始。在这种情况下,您实施的基本算法基本上非常糟糕。它检查数字是否为素数,但做了很多没有完成任何有用的工作。
虽然它可能不会影响速度,但我也发现使用一个检查单个数字是否为素数的函数更容易,并且只返回true
/ false
表示结果,而不是有一些精心设计的代码来确定前一个循环是完成还是提前退出。
几乎就是避免完全不必要的悲观化。
至少在我看来,使用std::async
启动线程也更容易(在这种情况下)。这让我们可以很容易地从我们的线程(我们想要的计数)中返回一个值。
所以,让我们先根据这些观察来修复prime
:
int prime(int a, int b)
{
int count = 0;
if (a == 2)
++count;
if (a % 2 == 0)
++a;
auto check = [](int i) -> bool {
for (int j = 3; j*j <= i; j += 2)
if (i % j == 0)
return false;
return true;
};
for (a; a <= b; a+=2) {
if (check(a))
++count;
}
return count;
}
现在,让我指出,这已经足够快(甚至是单线程),如果我们只是想让我们的工作完成速度提高4倍(或者更快),那么我们就可以从完美的线程中获得 - 缩放,我们已经完成,即使根本没有使用线程。对于你给出的限制,这将在1秒内完成。
然而,为了论证,我们假设我们想要获得更多,并且也使用多个核心。这里需要注意的一件事是,我们通常至少需要比核心更多的线程。问题很简单:每个核心只有一个线程,我们没有什么可以弥补我们甚至在线程之间没有真正分配负载的事实 - 处理最大数字的线程有相当多的工作要比处理最小数字的线程 - 但如果我们有(例如)一个4核机器,一旦一个线程完成,我们只能使用75%的CPU。然后当另一个线程完成时,它下降到50%。然后25%,最后只使用一个核心完成。
我们可能可能会进行一些计算以尝试更均匀地分配负载,但是将负载分成例如6到8倍的线程要容易得多。核心。这样,计算可以继续使用所有核心,直到只剩下三个线程 1 。
将所有内容放入代码中,我们最终会得到类似的结果:
int main() {
using namespace chrono;
int limit = 50000000;
unsigned int nthreads = 8 * thread::hardware_concurrency();
cout << "\nComputing multi-threaded:\n";
cout << "Number of threads: " << nthreads << "\n";
cout << "Calculating number of primes less than " << limit << "... \n";
auto start2 = high_resolution_clock::now();
vector<future<int>> threads;
int intrvl = limit / nthreads;
for (int i = 0; i < nthreads; i++)
threads.emplace_back(std::async(std::launch::async, prime, i*intrvl + 1, (i + 1)*intrvl));
int primes = 0;
for (auto &t : threads)
primes += t.get();
auto end2 = high_resolution_clock::now();
cout << "Primes: " << primes << ", Time: " << duration_cast<milliseconds>(end2 - start2).count() << "\n";
}
请注意几点:
至少当我运行它时,它似乎就像我们期望/希望一样:它使用100%的CPU时间直到它非常接近结束,当它在完成之前开始下降(也就是说,当我们执行的线程少于执行它们的核心时)。
答案 2 :(得分:1)
OpenMP示例正在使用&#34; reduction&#34;在sum变量primes
上,这意味着每个任务总结了自己的本地primes
变量。
OpenMP在并行部分的末尾将primes
的线程本地副本添加到一起以获得总计。
这意味着它不需要锁定。
正如@Sam所说,如果一个线程无法获取互斥锁,它将进入休眠状态。
所以在你的情况下,线程会花费相当多的时间睡着。
如果您不想使用OpenMP,请尝试使用static std::atomic<int> primes = 0;
,然后您就不需要使用互斥锁并解锁。
或者您可以使用数组primes[numThreads]
来模拟OpenMP减少,其中线程i
总和为primes[i]
,然后在末尾加primes[]
。