CPU使用率高的常见原因是什么

What are the common causes for high CPU usage?

本文关键字:是什么 常见原 使用率 CPU      更新时间:2023-10-16

背景:

在我用C++编写的应用程序中,我创建了3个线程:

  • AnalysisThread(或Producer):它读取输入文件,对其进行解析,生成模式,并将它们排入std::queue1
  • PatternIdRequestThread(或Consumer):它从队列中取出模式,并通过客户端(用C++编写)将它们逐个发送到数据库,客户端返回模式uid,然后将其分配给相应的模式
  • ResultPersistenceThread:它只做很少的事情,和数据库对话,而且就CPU使用而言,它工作得很好

前两个线程占用60-80%的CPU使用率,每个线程平均占用35%。

问题:

我不明白为什么有些线程占用大量CPU。

我的分析如下:如果是操作系统决定上下文切换、中断和调度哪个线程应该访问系统资源,比如CPU时间,那么为什么进程中的一些线程碰巧比其他线程使用更多的CPU?看起来一些线程在枪口下强行从操作系统中夺走CPU,或者操作系统对一些线程有真正的弱点,所以它从一开始就偏向于它们,给了它们所有的资源。为什么它不能做到公正,平等地给予所有人?

我知道这太天真了。但如果我沿着这条线思考,我会更加困惑:操作系统根据线程要做的工作量,将CPU访问权限授予线程,但在完全执行之前,操作系统如何计算或预测的工作量?

我想知道CPU使用率高的原因是什么?我们如何识别他们?是否可以通过查看代码来识别它们?工具是什么?

我正在使用Visual Studio 2010。

我对std::queue也有疑问。我知道标准容器是不安全的。但是,如果正好有一个线程将项目排入队列,那么如果正好有个线程从中取出项目,安全吗?我想象它就像一个管道,一边插入数据,另一边删除数据,那么如果同时进行,为什么会不安全呢?但这并不是本主题中的真正问题,不过,您可以在答案中添加注释,以解决此问题

更新:

在我意识到我的消费者线程正在使用忙旋转后,我用Sleep修复了3秒钟。此修复是暂时的,很快我将使用Event。但即使使用了Sleep,CPU使用率也下降到了30-40%,偶尔还会上升到50%,从可用性的角度来看,这似乎并不理想,因为系统对用户当前使用的其他应用程序没有响应。

在高CPU使用率的情况下,有什么方法可以改进吗?如前所述,生产者线程(现在使用大部分CPU周期)读取文件,解析其中的数据包(某种格式),并从中生成模式。如果我使用睡眠,那么CPU使用量会减少,但这是个好主意吗?常见的解决方法是什么?

就我个人而言,如果我的线程有工作要做,并且我的机器上有空闲的内核,我会非常恼火,因为操作系统没有给它们高CPU使用率。所以我真的不认为这里有任何问题[Edit:原来你的繁忙循环是个问题,但原则上高CPU使用率没有错]。

操作系统/调度程序几乎无法预测线程将要做的工作量。线程处于以下三种状态之一:

  1. 阻止等待某些东西(睡眠、互斥、I/O等)
  2. 可运行,但当前未运行,因为其他内容
  3. 正在运行

调度程序将选择尽可能多的内核(或超线程,无论什么)来运行,并运行每一个内核,直到它阻塞,或者直到称为"时间片"的任意时间段到期。如果可以的话,它会安排其他事情。

因此,如果一个线程将大部分时间花在计算上,而不是阻塞上,并且如果有空闲的内核,那么它将占用大量的CPU时间。

中有很多关于调度程序如何根据优先级等因素选择运行内容的细节。但基本的想法是,有很多事情要做的线程不需要被预测为计算量大,只要有什么事情需要调度,它就会一直可用,因此往往会被调度。

对于您的示例循环,您的代码实际上并没有做任何事情,所以在判断5-7%CPU是否合理之前,您需要检查它是如何优化的。理想情况下,在双核心机器上,处理重线程应该占用50%的CPU。在4芯机器上,25%。因此,除非你至少有16个内核,否则你的结果乍一看是异常的(如果你有16个核心,那么一个线程占35%会更异常!)。在标准的桌面操作系统中,大多数内核大部分时间都处于空闲状态,因此实际程序运行时占用的CPU比例越高越好。

在我的机器上,当我运行主要是解析文本的代码时,我经常使用一个核心的CPU。

如果只有一个线程将项目排入队列,那么如果正好有一个线程从中删除项目?

不,这对于使用标准容器的std::queue来说是不安全的。std::queue是序列容器(vectordequelist)之上的一个薄包装器,它不添加任何线程安全性。添加项的线程和删除项的线程共同修改一些数据,例如底层容器的size字段。您要么需要一些同步,要么需要一个安全的无锁队列结构,该结构依赖于对公共数据的原子访问。std::queue两者都没有。

编辑:好的,由于您正在使用繁忙旋转来阻塞队列,这很可能是CPU使用率高的原因。操作系统的印象是,线程在做有用的工作,而实际上并没有,所以它们占用了全部CPU时间。这里有一个有趣的讨论:在java中检查另一个线程布尔值,哪一个对性能更好

我建议您切换到事件或其他阻塞机制,或者使用一些同步队列,看看进展如何。

此外,关于队列是线程安全的"因为只有两个线程在使用它"的推理是非常危险的。

假设队列被实现为链表,想象一下如果它只剩下一两个元素会发生什么。既然你无法控制生产者和消费者的相对速度,那么情况很可能就是这样,所以你就有大麻烦了。

在开始考虑如何优化线程以消耗更少的CPU之前,您需要了解所有的CPU时间花在哪里。获取此信息的一种方法是使用CPU探查器。如果你没有,那就试试"瞌睡虫"吧。它很容易使用,而且是免费的。

CPU探查器将监视您正在运行的应用程序,并记下所花费的时间。因此,它将为您提供一个函数列表,按它们在采样期间使用了多少CPU、调用了多少次等进行排序。现在,您需要从最占用CPU的函数开始查看分析结果,看看您可以对这些函数进行哪些更改以减少CPU使用。

重要的是,一旦你有了分析器结果,你就有了实际的数据,告诉你可以优化应用程序的哪些部分以获得最大的回报。

现在让我们来考虑一下你能找到的消耗大量CPU的东西。

  • 工作线程通常被实现为一个循环。在循环的顶部进行检查,以确定是否有工作要做,并且是否执行任何可用的工作。循环的新迭代再次开始循环。

    您可能会发现,使用这样的设置,分配给该线程的大部分CPU时间都花在了循环和检查上,而实际工作只花了很少的时间。这就是所谓的忙等待问题。为了部分解决这个问题,可以在循环迭代之间添加sleep,但这不是最好的解决方案。解决这个问题的理想方法是,当没有工作要做时,让线程进入睡眠状态,当其他线程为睡眠线程生成工作时,它会发送一个信号来唤醒它。这实际上消除了循环开销,线程只会在有工作要做的时候使用CPU。我通常用信号量来实现这种机制,但在Windows上,您也可以使用Event对象。以下是一个实现的示意图:

    class MyThread {
    private:
        void thread_function() {
            while (!exit()) {
                if (there_is_work_to_do())
                    do_work();
                go_to_sleep();
            }
        }
        // this is called by the thread function when it
        // doesn't have any more work to do
        void go_to_sleep() {
            sem.wait();
        }
    public:
        // this is called by other threads after they add work to
        // the thread's queue
        void wake_up() {
            sem.signal();
        }
    };
    

    请注意,在上述解决方案中,线程函数总是在执行一个任务后尝试进入睡眠状态。如果线程的队列有更多的工作项,那么对信号量的等待将立即返回,因为每次向队列中添加项时,发起方都必须调用wake_up()函数。

  • 您可能在探查器输出中看到的另一件事是,大部分CPU都花在了工作线程在工作时执行的函数上。这实际上并不是一件坏事,如果大部分时间都花在了工作上,那么这意味着线程有工作要做,并且有CPU时间可以做这些工作,所以原则上没有错。

    但是,您可能对应用程序使用如此多的CPU感到不高兴,所以您需要寻找优化代码的方法,以便它更有效地完成工作。

    例如,你可能会发现一些小的辅助函数被调用了数百万次,所以虽然函数的一次运行很快,但如果你把它乘以几百万,它就会成为线程的瓶颈。在这一点上,您应该考虑如何进行优化,以减少该函数中的CPU使用,可以通过优化其代码,也可以通过优化调用方来减少调用该函数的次数。

    因此,这里的策略是根据分析报告从最昂贵的函数开始,并尝试进行小的优化。然后,您重新运行探查器来查看情况是如何变化的。你可能会发现,对CPU最密集的功能进行一个小的更改,就会将其降到第二或第三位,因此整体CPU使用量减少了。在你为自己的进步表示祝贺之后,你用新的顶部函数重复这个练习。您可以继续这个过程,直到您对您的应用程序的效率感到满意

祝你好运。

尽管其他人已经正确地分析了问题(据我所知),但让我尝试为提出的解决方案添加更多细节。

首先,总结存在的问题:1.如果你让你的消费者线程在for循环或类似的循环中忙于旋转,那就是对CPU功率的严重浪费。2.如果使用固定毫秒数的sleep()函数,要么也是对CPU的浪费(如果时间量太低),要么是不必要地延迟了进程(如果时间太高)。没有办法将时间设置得恰到好处。

您需要做的是使用一种在正确的时刻醒来的睡眠类型,即每当新任务被添加到队列时。

我将解释如何使用POSIX来实现这一点。我意识到,当您使用Windows时,这并不理想,但为了从中受益,您可以使用Windows的POSIX库,也可以使用环境中可用的相应函数。

步骤1:您需要一个互斥和一个信号:

#include <pthread.h>
pthread_mutex_t *mutex  = new pthread_mutex_t;
pthread_cond_t  *signal = new pthread_cond_t;
/* Initialize the mutex and the signal as below.
   Both functions return an error code. If that
   is not zero, you need to react to it. I will
   skip the details of this. */
pthread_mutex_init(mutex,0);
pthread_cond_init(signal,0);

步骤2:现在在使用者线程中,等待发送信号。这个想法是,每当生产者向队列添加了一个新任务时,它就会发送信号:

/* Lock the mutex. Again, this might return an error code. */
pthread_mutex_lock(mutex);
/* Wait for the signal. This unlocks the mutex and then 'immediately'
   falls asleep. So this is what replaces the busy spinning, or the
   fixed-time sleep. */
pthread_cond_wait(signal,mutex);
/* The program will reach this point only when a signal has been sent.
   In that case the above waiting function will have locked the mutex
   right away. We need to unlock it, so another thread (consumer or
   producer alike) can access the signal if needed.  */
pthread_mutex_unlock(mutex);
/* Next, pick a task from the queue and deal with it. */

上面的步骤2基本上应该放在一个无限循环中。确保有一种方法可以让流程脱离循环。例如,尽管有点粗糙,但您可以将一个"特殊"任务附加到队列中,意思是"脱离循环"。

第3步:使生产者线程在将任务附加到队列时发送信号:

/* We assume we are now in the producer thread and have just appended
   a task to the queue. */
/* First we lock the mutex. This must be THE SAME mutex object as used
   in the consumer thread. */
pthread_mutex_lock(mutex);
/* Then send the signal. The argument must also refer to THE SAME
   signal object as is used by the consumer. */
pthread_cond_signal(signal);
/* Unlock the mutex so other threads (producers or consumers alike) can
   make use of the signal. */
pthread_mutex_unlock(mutex);

第4步:当一切都完成并关闭线程时,必须销毁互斥锁和信号:

pthread_mutex_destroy(mutex);
pthread_cond_destroy(signal);
delete mutex;
delete signal;

最后,让我重复一下其他人已经说过的一件事:您不能使用普通的std::deque进行并发访问。解决这个问题的一种方法是声明另一个互斥体,在每次访问deque之前将其锁定,然后立即解锁。

编辑:根据评论,再多说几句关于制作人线程的话。据我所知,生产者线程目前可以自由地向队列中添加尽可能多的任务。所以我想它会继续这样做,并让CPU保持忙碌,直到它不会被IO和内存访问延迟。首先,我不认为由此导致的高CPU使用率是一个问题,而是一个好处。然而,一个严重的问题是队列将无限期增长,可能导致进程内存空间不足。因此,一个有用的预防措施是将队列的大小限制在合理的最大值,并在队列过长时让生产者线程暂停。

为了实现这一点,生产者线程将在添加新项之前检查队列的长度。如果它已满,它将进入睡眠状态,等待消费者在将任务从队列中删除时发送信号。为此,您可以使用第二种信号机制,类似于上面描述的机制。

线程消耗内存等资源。阻塞/取消阻塞线程会产生一次性开销。如果线程每秒阻塞/取消阻塞数万次,这可能会浪费大量的CPU。

然而,一旦线程被阻塞,不管阻塞多长时间,都不会有持续的成本。发现性能问题的常用方法是使用profiler。

然而,我经常这样做,我的方法是:http://www.wikihow.com/Optimize-Your-Program%27s-性能

正如人们所说,同步生产者和消费者线程之间切换的正确方法是使用条件变量。当生产者想要向队列中添加元素时,它会锁定条件变量,添加元素,并通知服务员条件变量。使用者等待相同的条件变量,并在收到通知时,使用队列中的元素,然后再次锁定。我个人建议使用boost::interprocess,但也可以使用其他API以相当简单的方式完成。

此外,需要记住的一点是,虽然从概念上讲,每个线程只在队列的一端操作,但大多数库都实现了O(1)count()方法,这意味着它们有一个成员变量来跟踪元素的数量,这是一个罕见且难以诊断并发问题的机会。

如果你正在寻找一种方法来减少消费者线程的cpu使用量(是的,我知道这是你真正的问题)。。。好吧,听起来它实际上在做它现在应该做的事情,但数据处理是昂贵的。如果你能分析它在做什么,可能会有优化的机会。

如果你想智能地调节生产者线程。。。这需要更多的工作,但您可以让生产者线程将项目添加到队列中,直到它达到某个阈值(比如10个元素),然后等待不同的条件变量。当消费者消耗了足够的数据,导致排队元素的数量低于阈值(比如5个元素)时,它会通知第二个条件变量。如果系统的所有部分都能快速移动数据,那么这仍然会消耗大量的CPU,但数据在它们之间的分布会相对均匀。在这一点上,操作系统应该负责让其他不相关的进程获得CPU的公平份额。

线程CPU的使用取决于许多因素,但总体而言,操作系统只能根据中断线程的点来分配处理时间。

如果线程无论如何都与硬件交互,那么操作系统就有机会中断线程并将处理分配到其他地方,主要是基于硬件交互需要时间的假设。在您的示例中,您使用iostream库,从而与硬件进行交互。

如果你的循环没有这个,那么它很可能会使用几乎100%的cpu。

  1. 使用异步(文件和套接字)IO来减少无用的CPU等待时间
  2. 如果可能的话,使用垂直线程模型来减少上下文切换
  3. 使用无锁数据结构
  4. 使用评测工具(如VTune)找出热点并进行优化