如何提高多线程程序的cpu利用率

正如大家所知道的那样,多核多cpu越来越普遍了,而且编写多线程程序也是件很简单的事情。在Windows下面,调用CreateThread函数一次就能够以你想要的函数地址新建一个子线程运行。然后,事情确实你发现创建多线程根本没有让程序快多少,也没有提高多少cpu利用率,甚至可能让cpu利用率下降。唯一能够确定的是多线程能够避免界面假死。为什么会是这样的了。本文将举一些例子和讲述一些原因。

首先,我来讲一下多处理的一些知识。如下图所示,

多处理器系统也只有一个待运行的线程队列,内存中也只有一个操作系统拷贝,而且也只有一个内存系统,但是会有多个cpu同时运行不同的线程。一个cpu运行一个线程,那么上图中的系统最多能在同一时间运行2个线程。其实,多处理系统需要掌握的知识不是这些,而是缓存一致性
现在来解释下什么是缓存一致性。由于,还是只有一个内存系统。所有cpu都要和这个内存系统通信,但是只有一条总线,那么这无疑会造成总线紧张,限制整体的速度了。那么,你多个cpu也没多少意义了。解决这个问题的办法还是利用cpu的缓存机制,学过组成原理的同学都知道,cpu的缓存命中率还是很高的,有90%以上吧。那么,我继续利用缓存机制还是可以降低总线的频繁使用的。但是,每个cpu都有自己的缓存。如果有2个cpu的缓存存储的是同一内存数据的内容,其中一个cpu的缓存更新了,另外一个cpu的缓存也必须更新,这就是所谓的缓存一致性。编程多线程程序的一个很重要的一点就是避免因为缓存一致性引起的缓存更新风暴。
现在我举一个缓存更新风暴的例子。
如图所示的类定义,
缓存一致性风暴
锁lockHttp和lockSsl中间只有8个字节,而绝大部分系统上一个缓存行是128个字节,那么这2个锁很可能就处在同一个缓存行上面。那么,最坏的情况会发生什么事情了。假设处理器P1在运行一个处理http请求的线程T1,处理器P2在运行一个处理ssl请求的线程T2,那么当T1获得锁lockHttp的时候,锁的内容就会改变,为了保持缓存一致性,就会更新P2的缓存。那么,T2要获得锁lockssl的时候,发现缓存已经失效了,就必须从内存中重新加载缓存之类。总之,这会将缓存命中率降低到90%以下,引起性能的严重降低。而且发生这种事情的原因是因为我们不了解硬件的体系结构。

多cpu不能成倍提高速度的原因是任务的某些部分是必须串行处理的。比如,矩阵乘法可以分为三个部分,初始化矩阵,相乘,返回结果。这三部分第二部分可以用多线程来处理,第一部分和第三部分则是不可以的。而且第二部分必须在第一部分完成之后,第三部分必须在第一部分完成之后。那么,无论你添加多少个处理器,最快的时间都至少是第一部分和第二部分的时间之和。这个事实好像叫做Amdahl法则。
如果使用多线程,那么就必须考虑线程同步,而线程同步又是导致速度降低的关键。所以下面就讲述一些方法来加快多线程程序的吞吐速度。
方法一,把一个任务分解为多个可以子任务。
因为总有些子任务是可以并发的,多个子任务并发执行了很可能就能够避免cpu需要io操作的完成了,而且能够提高系统的吞吐量。
方法二,缓存多线程的共享数据。
当你已经在使用多线程了,很多时候必须使用共享数据。如果,数据是只读的,那么可以在第一次获取后保存起来,以后就可以重复使用了。但是,第一次的获取还是无法避免的需要线程同步操作的。
方法三,如果线程数目有限,就不要共享数据。
做法是为每一个线程实例化一个单独的数据,其实就是为每一个线程分配一块数据使用。这样没有线程同步操作了,速度可以尽可能的提示。
方法四,如果没办法确定线程数目到底有多少,那么使用部分共享吧。
部分共享其实就是使用多个资源池代替一个资源池,资源池的数目得更加经验来确定。如下图所示,

最后在提一个叫做Thundering Herd的问题,该问题维基百科有定义http://en.wikipedia.org/wiki/Thundering_herd_problem。大意是,当多个线程在等待一个资源的时候,如果事件等待到了,操作系统是唤醒所有等待的线程让它们自己去竞争资源了还是选择一个线程把资源给它。当然唤醒所有的线程肯定开销要大,而且所有没有抢到资源的线程还得重新进入等待状态,这无疑造成很多没必要的操作,浪费了没必要的线程上下文切换。总之,会不会存在Thundering Herd还是跟不同的操作系统有关的。万一存在Thundering Herd了,多线程可能就没那么好办了。

到现在我们知道了为什么多cpu并不能成倍提高程序的速度了。首先因为有些任务无法并行,其次即使是并行cpu之间还是有很多牵制的。本书的内容主要来自提高c++性能的编程技术一书。