无法从线程类获得100%的CPU使用率

时间:2016-06-05 01:01:26

标签: c++ multithreading

我试图通过线程类写出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();
        }
    }
}

但是当我运行它时,我得到以下图表

enter image description here

那是正弦曲线。但是当我运行使用openmp的@Mysticial回答时,我得到了这个

enter image description here

我通过ps -eLf检查了两个程序,并且它们都使用了8个线程。为什么我得到这个不稳定的图表?如何获得与openmp对线程相同的结果?

3 个答案:

答案 0 :(得分:7)

Mystical's answer与您的代码之间存在一些根本区别。

差异#1

您的代码为每个CPU创建了一大块工作,并让它运行完成。这意味着一旦线程完成,CPU使用率将急剧下降,因为CPU将处于空闲状态,而其他线程将运行完成。这是因为调度并不总是公平的。一个线程可能会比其他线程更快地完成并完成。

OpenMP解决方案通过声明schedule(dynamic)来解决这个问题,它告诉OpenMP在内部创建一个所有线程将使用的工作队列。当一大块工作完成后,在代码中退出的线程将消耗另一部分工作并忙于工作。

最终,这成为挑选足够大小的块的平衡行为。太大,并且在任务结束时CPU可能不会超出。太小了,可能会有很大的开销。

差异#2

您正在写入在所有线程之间共享的变量primes。 这有两个后果:

  • 需要同步才能阻止data race
  • 它使现代CPU上的缓存非常不高兴,因为在一个线程上的写入对另一个线程可见之前需要缓存刷新。

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。

CPU Usage Commandline

展望未来......

您可能已经注意到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通常在封面下有一个相当复杂的线程池,所以匹配它(确切地说)可能至少有些困难。

其次,在我看来,在优化线程之前, 应该尝试以至少一半的正常基本算法开始。在这种情况下,您实施的基本算法基本上非常糟糕。它检查数字是否为素数,但做了很多没有完成任何有用的工作。

  1. 检查偶数是否为素数。除了2,他们不是。如初。
  2. 检查奇数是否可以被偶数整除。再说一遍,他们不是。如初。
  3. 检查数字是否可以被大于其平方根的数字整除。如果没有小于平方根的除数,那么也不能比平方根大一个。
  4. 虽然它可能不会影响速度,但我也发现使用一个检查单个数字是否为素数的函数更容易,并且只返回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";
    }
    

    请注意几点:

    1. 运行得足够快,我将上限提高了一个相当大的因素,所以它运行得足够长,我们至少可以看到它使用100%的CPU时间几秒钟在它完成之前 2
    2. 我添加了一些计时代码,以便更准确地了解其运行时间。
    3. 至少当我运行它时,它似乎就像我们期望/希望一样:它使用100%的CPU时间直到它非常接近结束,当它在完成之前开始下降(也就是说,当我们执行的线程少于执行它们的核心时)。

      enter image description here

      1. 如果您想知道OpenMP如何避免这种情况:它通常使用线程池,因此循环的一些迭代次数将作为任务分派给线程池。这使得它可以生成大量任务,而无需同时争用CPU时间的大量线程。
      2. 根据您使用的上限,它在我的机器上完成大约90毫秒,这不足以让它甚至在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[]