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

17

背景:

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

  • AnalysisThread(或生产者):它读取一个输入文件,解析它并生成模式,并将其加入到std::queue1中。
  • PatternIdRequestThread(或消费者):它从队列中出队模式,并通过客户端(用C++编写)将它们逐个发送到数据库,该客户端返回模式的uid,然后将其分配给相应的模式。
  • ResultPersistenceThread:它还做了一些其他事情,与数据库通信,就CPU使用率而言,它的表现很好。

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

问题:

我不明白为什么有些线程会占用高CPU使用率。

我将其分析如下:如果操作系统决定像context-switch, interruptscheduling这样的决策,以确定哪个线程应该访问系统资源(如CPU时间),那么为什么同一个进程中的一些线程会比其他线程使用更多的CPU?看起来像一些线程强制从操作系统枪口下获取CPU,或者操作系统对某些线程有着真正的偏爱,从一开始就对它们有偏见,给予它们所有的资源。为什么它不能公正地给予它们平等的资源呢?
我知道这是天真的想法。但是,如果我沿着这条路线思考,我会更加困惑:操作系统根据线程要完成的工作量来给线程访问CPU的权限,但是在完全执行之前,操作系统如何计算或预测工作量呢?
我想知道高CPU使用率的原因是什么?我们如何识别它们?是否可能仅通过查看代码来识别它们?有哪些工具可用?
我正在使用Visual Studio 2010。
1. 我对std :: queue也有疑问。我知道标准容器不是线程安全的。但是,如果恰好有一个线程将项目排队到队列中,则如果恰好有一个线程从中提取项目,则安全吗?我想象它就像一条管道,在一端插入数据,在另一端删除数据,那么如果同时进行,为什么会不安全呢?但这不是本主题的真正问题,您可以在答案中添加注释以解决此问题。
更新:

我发现我的消费线程正在使用忙等待,现在我已经用Sleep解决了3秒钟。这个修复是临时的,很快我会使用Event替代它。但即使使用Sleep,CPU使用率也降至30-40%,偶尔会上升到50%,从可用性角度来看,这似乎并不理想,因为系统无法响应用户当前正在使用的其他应用程序。

有没有办法仍然可以改善高CPU使用率?如前所述,生产线程(现在使用大多数CPU周期)读取文件,在其中解析数据包(某种格式),并生成模式。如果使用sleep,则CPU使用率将减少,但这是一个好主意吗?有哪些常见方法可以解决这个问题?


2
你的测试代码很可能是IO限制,这就是为什么你看不到更高的百分比(你使用 std::endl 在每次写入后清空缓冲区)。 - Xeo
2
如果队列已满/空,你是如何制作生产者/消费者块的?事件?忙旋转? - Tudor
@Tudor:目前我正在使用繁忙自旋。但是这是否会引起这么多问题? - Nawaz
4
是的,那很可能是原因。 - Tudor
4
一旦有任何线程修改了std::queue(无论是哪种方式),所有对它的访问都必须受到保护。使用条件变量。(虽然可以使用原子变量在实现中实现无锁队列,但这不是std::queue。) - James Kanze
显示剩余11条评论
8个回答

27
个人而言,如果我的线程有工作要做,而我的机器上有空闲的核心,因为操作系统没有给它们高CPU使用率,我会感到相当恼火。所以我并不认为这里有任何问题[编辑:事实证明,您的忙循环是一个问题,但原则上高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 都没有。


我已经在我的问题中发布了更新。希望你能看到。 :-) - Nawaz
@Nawaz:我无法确定是什么在占用CPU。您有没有可能对其进行性能分析并找出问题所在?如果这是一台2核心的机器,而CPU峰值为50%,那就意味着有一个线程正在全速运行,尽管也可能有两个线程来回传递,没有有效的并行处理。我很惊讶它会影响到操作系统的其余部分,肯定出了问题,但我不知道是什么。 - Steve Jessop
2
@Nawaz:通常最好的“分析”形式是“堆栈快照”。在调试器中中断程序,查看您所在的位置(无论您的代码在堆栈跟踪中结束并且Citrix的代码开始)。重复此操作5次,任何您两次到达的地方都是热点代码,请确定是否认为该代码对于您的程序(以及特定线程)花费所有时间是合理的。 - Steve Jessop
2
低线程优先级通常不会产生任何影响。你的程序正在做很多“某事”。低优先级只意味着如果操作系统有其他任务要执行,那么它将安排执行其他任务。这可能会使您的计算机更加响应,但它并不能解决根本问题,即您的程序正在做比您认为应该做的更多的“某事”。您需要找出它正在做什么,“某事”,以及是否应该减少它的工作量。您已经消除了繁忙循环(至少我认为您已经这样做了,因为 Tudor 告诉过您这是不好的),所以您需要找到下一个问题。 - Steve Jessop
@Tudor:我已经在CodeReview上发布了concurrent_blocking_queue的实现。如果感兴趣,请在这里查看:http://codereview.stackexchange.com/questions/12052/implementation-of-concurrent-blocking-queue-for-producer-consumer - Nawaz
显示剩余5条评论

7

编辑:好的,既然你正在使用繁忙自旋来阻塞队列,这很可能是高CPU使用率的原因。操作系统认为你的线程正在执行有用的工作,而实际上它们并没有,所以它们占用了全部CPU时间。这里有一个有趣的讨论:在Java中检查另一个线程的布尔值哪个性能更好

我建议你要么切换到事件或其他阻塞机制,要么使用一些同步队列,看看情况如何。

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

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


你说得对,我会在获取更多上下文后解决它,但我认为这是一个非常重要的问题,值得一些评论。 - Tudor
所以,我已经根据你提供的新上下文进行了编辑。 - Tudor
我不明白为什么你在忙旋转的情况下谈论“同步队列”。 “同步队列”不是关于线程安全队列吗? - Nawaz
@Nawaz: 是的,但我的意思是像TBB中已经实现的并发队列一样。 - Tudor
3
TBB的concurrent_bounded_queue有一个阻塞的pop()方法,可以避免忙等待。或者,您可以使用非阻塞的concurrent_queue,并附带一个信号量来计数/等待。 - Martin James
显示剩余3条评论

5
在考虑如何优化线程以减少 CPU 使用前,您需要了解 CPU 时间都用在哪里。一种获取相关信息的方法是使用 CPU 分析器。如果您没有一个,可以尝试使用 Very Sleepy。它易于使用且免费。
CPU 分析器将监视正在运行的应用程序并记录时间花费在哪里。结果,它会给出一个按 CPU 使用量排序、被调用次数等方式整理的函数列表。现在,您需要从使用 CPU 最多的函数开始查看分析结果,并查看如何改变这些内容以减少 CPU 使用率。
重要的是,一旦您获得了分析结果,您就拥有实际数据,可以告诉您哪些部分可以进行优化,以获得最大收益。
现在我们来考虑可能在消耗大量 CPU 的情况下发现的内容。
  • A worker thread is typically implemented as a loop. At the top of the loop a check is made to decide if there is work to do and any available work is executed. A new iteration of the loop begins the cycle again.

    You may find that with a setup like this most of the CPU time allocated to this thread is spent looping and checking, and very little is spent actually doing work. This is the so called busy-wait problem. To partially address this you can add a sleep in between loop iterations, but this isn't the best solution. The ideal way to address this problem is to put the thread to sleep when there is no work to do, and when some other thread generates work for the sleeping thread it sends a signal to awaken it. This practically eliminates the looping overhead, the thread will only use CPU when there is work to do. I typically implement this mechanism with semaphores, but on Windows you can also use an Event object. Here is a sketch of an implementation:

    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();
        }
    };
    

    Note that in the above solution the thread function always tries to go to sleep after executing one task. If the thread's queue has more work items, then the wait on the semaphore will return immediately, since each time an item was added to the queue the originator must have called the wake_up() function.

  • The other thing you may see in the profiler output is that most of the CPU is spent in functions executed by the worker thread while it is doing work. This is actually not a bad thing, if most of the time is spent working, then that means that the thread had work to do and there was CPU time available to do that work, so in principle there is nothing wrong here.

    But still, you may not be happy that your application uses so much CPU, so then you need to look at ways to optimize your code so that it does the the work more efficiently.

    For example, you may find that some little auxiliary function was called millions of times, so while a single run of the function is quick, if you multiply that by a few million it becomes a bottle neck for the thread. At this point you should look at ways to make optimizations to reduce the CPU usage in this function, either by optimize its code, or by optimizing the caller(s) to call the function less times.

    So the strategy here is to start from the most expensive function according to the profiling report and try to make a small optimization. Then you rerun the profiler to see how things changed. You may find that a small change to the most CPU intensive function moves it down to 2nd or 3rd place, and as a result the overall CPU usage was reduced. After you congratulate yourself for the improvement, you repeat the exercise with the new top function. You can continue this process until you are satisfied that your application is as efficient as it can be.

祝你好运。


3
尽管其他人已经正确地分析了问题(据我所知),让我尝试为提出的解决方案增加一些细节。
首先,总结问题如下: 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);

第二步:现在在消费者线程中,等待信号被发送。理念是当生产者将新任务添加到队列时,会发送信号:

/* 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. */

第二步基本上应该放在一个无限循环中。确保有一种方式可以打破循环过程。例如-虽然稍显粗糙-您可以将一个“特殊”任务附加到队列中,意思是“打破循环”。

第三步:使生产者线程能够在向队列附加任务时发送信号:

/* 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);

第四步:当所有的任务都完成并且线程关闭后,你必须销毁互斥锁和信号量:

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

最后,我想再次强调其他人已经提到的一件事情:绝不能在并发访问时使用普通的std::deque。解决这个问题的方法之一是声明另一个互斥锁,在每次访问deque之前将其锁定,然后立即解锁它。 编辑:根据评论增加了一些关于生产者线程的说明。据我所知,目前生产者线程可以自由地向队列中添加尽可能多的任务。因此,我认为它会继续这样做,并且只要没有被IO和内存访问延迟,它就会保持CPU忙碌。首先,我不认为由此导致的高CPU使用率是一个问题,而是一种优势。然而,一个严重的担忧是队列将无限增长,潜在地导致进程耗尽内存空间。因此,有用的预防措施是将队列的大小限制在一个合理的最大值,并在队列过长时使生产者线程暂停。
为了实现这一点,生产者线程在添加新项之前将检查队列的长度。如果队列已满,它会将自己置于休眠状态,等待消费者发送信号以从队列中取出任务。为此,您可以使用第二个信号机制,类似于上面描述的那个。

@Nawaz:正如您更新(有关赏金)中明确说明的那样:“我正在寻找减少消费者线程所占用CPU使用率的技术。”它说的是“消费者”,而不是“生产者”。此外,我上面建议的内容会影响两个线程。 - jogojapan
@Nawaz:还有一件事。如果你打算实现信号,我建议你先这样做,并检查它是否解决了你的问题(可能在两个线程中)。 - jogojapan
哦,哇。这很令人困惑。虽然不是你的错。但我认为仍然要仔细实现并测试它在两个线程中的性能表现。 - jogojapan
@Nawaz: 你为什么认为使用信号或阻塞队列等阻塞机制无法解决问题?被阻塞的线程直到唤醒事件发生才会被操作系统调度。 - Tudor
@Tudor:信号机制如何阻塞生产者线程?消费者应该等待信号,对吧?当消费者等待时,生产者可以继续进行。 - Nawaz
显示剩余6条评论

3

线程会消耗诸如内存等资源。阻塞/解除阻塞线程会产生一次性成本。如果一个线程每秒阻塞/解除阻塞数万次,这将浪费大量的CPU。

然而,一旦线程被阻塞,无论它被阻塞多长时间,都没有持续性成本。发现性能问题的常用方法是使用分析工具。

然而,我经常使用以下方法进行优化:http://www.wikihow.com/Optimize-Your-Program%27s-Performance


1

线程 CPU 使用率取决于许多因素,但主要上操作系统只能根据可以中断线程的点分配处理时间。

如果您的线程以任何方式与硬件交互,则这为操作系统提供了中断线程并将处理分配到其他地方的机会,主要基于硬件交互需要时间的假设。在您的示例中,您正在使用 iostream 库,因此正在与硬件交互。

如果您的循环没有这个,则最有可能使用近乎 100% 的 CPU。


1
我相信Nawaz有一个带有抢占式多任务处理的操作系统,据我所知,这种技术是在Windows 95中向微软的客户介绍的,但在此之前已经存在于IBM的OS/2中。因此,操作系统几乎可以在任何地方中断线程(例外情况是内核模式下偶尔有一些禁用中断的代码片段,或者处于所谓的“原子”操作的中间)。 - Steve Jessop

1

正如其他人所说,同步生产者和消费者线程之间的交接正确的方法是使用条件变量。当生产者想要向队列中添加元素时,它会锁定条件变量,添加元素并通知条件变量上的等待者。消费者在同一条件变量上等待,当被通知后,从队列中消耗元素,然后再次锁定。我个人建议使用boost::interprocess来实现这些,但也可以使用其他API以相对简单的方式完成。

另外,需要记住的一件事是,尽管在概念上每个线程仅在队列的一端操作,但大多数库实现了O(1)的count()方法,这意味着它们有一个成员变量来跟踪元素数量,这是一种罕见且难以诊断的并发问题。

如果你正在寻找减少消费者线程CPU使用率的方法(是的,我知道这是你真正的问题)……嗯,听起来它现在实际上正在执行它应该执行的任务,但数据处理很昂贵。如果你能分析它正在做什么,可能会有优化的机会。

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


同时,记住不要长时间持有锁。保护共享数据(队列)免受并发访问,但在添加或删除元素后,您就不需要再保护了。 - bdow

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接