前言

从开始学习编程之后,就渐渐痴迷于技术,平时遇到购书满减活动时就忍不住买一堆书。前两天闲着无聊,翻开了去年买的《编程之美》,目录里的“让 CPU
占用率听你指挥”吸引力我的眼球。这一年来捣鼓数据挖掘和机器学习,总会关注代码运行效率,偶尔会思考如何提高 CPU、GPU 的利用率。于是马上翻开了这一节。

让 CPU 利用率听你指挥

翻开后是一道编程题(3星,需要查阅一些资料,在60分钟内完成)

写一个程序,让用户来决定 Windows 任务管理器(Task Manager)的 CPU 占用率。程序设计的越精简越好,语言不限。例如,可实现下面三种情况:

* CPU 和占用率固定在50%,为一条直线
* CPU 的占用率为一条直线,具体占用率由命令行参数决定(参数范围 1~100)
* CPU 的占用率状态是一条正弦曲线
怎么实现呢

稍微想了想,如果想让 CPU 跑满,写一个死循环就好了,让 CPU 一直处于运行状态,那 50%
的利用率要怎么实现呢?一半时间运行一半时间休息,emmmmm。。休息。。突然想到了多线程里常用到的 sleep。接着往下看,确实是使用 sleep。

那就写写代码吧
while True: for i in range(7200000): pass time.sleep(0.01)
这里稍微解释下为什么是 7200000,以及为什么睡眠 0.01s(10ms)。

笔记本的 CPU 是 1.8Ghz,每秒运行次数大概为 1.8 * 10^9 次,假设 CPU 每个时钟周期可以执行两条代码,然后对于一段 for
循环代码,转换成汇编如下
next: mov eax, dword ptr[i] ; i放入寄存器 add eax, 1 ; 寄存器+1 mov dword ptr [i], eax
; 寄存器赋回i cmp eax, dword ptr [i] ; 比较i和n j1 next ; i小于n时重复循环
即5条代码,所以,1S 内循环次数为 1.8 * 10^9 * 2 / 5 = 720000000。而睡眠 10ms 是因为接近 Windows
的调度时间片。

运行了一下,只是稳定在 30% 左右,暂时先不调整循环次数,接着往后看。

可以看出来,这样设置利用率很麻烦,那有没有什么方法可以快点设置呢?

重新看看上面这段代码, 7200000 次循环花费的时间大约为 10ms,那意思就是 CPU 运行 10ms 然后再休息 10ms,再运行 10ms 再休息
10ms,接着运行 10ms 然后再休息 10ms ······ 想必肯定看出来什么了吧,我们只需要设置 CPU 运行多少时间就好了!于是可以写出下面代码
busyTime = 0.01 while True: startTime = time.clock() while((time.clock() -
startTime) <= busyTime): pass time.sleep(busyTime)
运行一下,跟刚刚差不太多,稳定在 30% 左右

正弦函数

这时候,我们也可以很容易就写出跑成正弦函数图像的代码了,不断改变运行与空闲的时间比就好了。
import time import mathimport affinity from multiprocessing import Process,
cpu_countdef exec_fun(): SAMPLING_COUNT = 200 # 抽样点数量 PI = math.pi # pi
TOTAL_AMPLITUDE = 300# 每个抽样点对应时间片 busySpan = [] amplitude = TOTAL_AMPLITUDE / 2
radianIncrement= 2.0 / SAMPLING_COUNT radian = 0.0 for i in
range(SAMPLING_COUNT): busySpan.append((amplitude+ math.sin(PI * radian) *
amplitude) / 1000.0) radian += radianIncrement # print(busySpan[i],
TOTAL_AMPLITUDE - busySpan[i]) j = 0 while True: startTime = time.clock() #
print(startTime) while ((time.clock() - startTime) <= busySpan[j]): pass #
print('sleep') time.sleep(0.3 - busySpan[j]) j = (j + 1) % SAMPLING_COUNT
exec_fun()
运行一下。emmmmmmmmmmmm。。。。等一下,不对啊,怎么不是正弦函数形状呢?



 

 这跟说好的好像不太一样啊。是不是因为用的是 python,跑的本来就慢的原因?那试试 C++ 吧
#include<stdlib.h> #include<Windows.h> #include<math.h> const int
SAMPLING_COUNT =150; const double PI = 3.1415926535; const int TOTAL_AMPLITUDE =
300; int main() { DWORD busySpan[SAMPLING_COUNT]; int amplitude =
TOTAL_AMPLITUDE /2; double radian = 0.0; double radianIncrement = 2.0 / (double
)SAMPLING_COUNT;for (int i = 0; i < SAMPLING_COUNT; i++) { busySpan[i] =
(DWORD)(amplitude + sin(radian * PI) * amplitude); radian += radianIncrement;
printf("%d\t%d\n", busySpan[i], TOTAL_AMPLITUDE - busySpan[i]); } DWORD
startTime= 0; for (int j = 0;; j = (j + 1) % SAMPLING_COUNT) { startTime =
GetTickCount();while ((GetTickCount() - startTime) <= busySpan[j]);
Sleep(TOTAL_AMPLITUDE- busySpan[j]); } return 0; }
 

再运行一下,它怎么还是这样???



于是乎捣鼓了 2 个小时。。。

……

……

……

后来仔细想了想,CPU 是 4 核 8 处理器的,不会是任务分摊到了几个处理器上了吧?于是查了查如何把当前进程放在一个处理器上执行。
if __name__ == "__main__": p = Process(target=exec_fun) p.start() pid = p.pid
print(affinity.get_process_affinity_mask(pid))
affinity.set_process_affinity_mask(pid,1)
运行一下,好的,它成了!!!



 

 顺便解决下上面C++的代码,在 main() 函数最开始加入下面代码
SetThreadAffinityMask(GetCurrentThread(), 1);
小节

好久没有这样子捣鼓过东西了,想想上次做操作系统课设的时候,要获取系统的信息,当时只是为了完成任务就没有去深究一些东西,这次捣鼓了 CPU
的利用率控制之后,对进程、CPU 以及 python 的多线程等知识又多了一点了解。感觉技术还是需要沉下心来才能学得好。