Not Only Algorithm,不仅仅是算法,关注数学、算法、数据结构、程序员笔试面试以及一切涉及计算机编程之美的内容 。。
你的位置:NoAlGo博客 » 趣味 » , ,

CPU占用率正弦曲线

编程之美上有这么一道题:写一个程序,让用户来决定Windows任务管理器(Task Manager)的CPU占用率。程序越精简越好,计算机语言不限。例如,可以实现下面三种情况:

  1. CPU的占用率固定在50%,为一条直线;
  2. CPU的占用率为一条直线,但是具体占用率由命令行参数决定(参数范围1~ 100);
  3. CPU的占用率状态是一个正弦曲线。

以前一开始看的时候就觉得很有意思,不过总没有运行成功,最近又开始看这本书,于是就真正地来实现一把(跟书上的实现不太一致,加入了自己的理解和修改,并添加多核CPU的处理)。

一 程序目的

这里实现的第3种方案,通过程序控制Windows任务管理器的CPU占用率为一条正弦曲线。其实画不同曲线的思想都是一样的,可以根据这个程序类似求出。

本人的计算机是四核的,所以任务管理器上有四条曲线,因而程序也对应地画出了四条正弦曲线。最终实现的效果图如下:

整体效果

其中单个核的放大图如下所示:

单个效果

二 算法实现

这个问题最初看上去可能是毫无思路,但沿着文中的分析一步步走下来,发现其实方法真的非常简单,本文实现的这个程序总代码量不超过40行。

首先是要弄清楚CPU的占用率曲线是怎么得到的。系统每一段时间会刷新CPU的占用率,占用率是用这段时间内CPU忙碌的时间跟总时间的比值计算得到。于是,要得到一个给定的CPU占用率(比如说25%),我们可以在这段时间的前25%时间段内让CPU一直在忙碌地运算,后面75%的时间让CPU不做任何事情,然后这段时间过后系统刷新CPU的占用率时就会得到我们想要的25%。

怎样让CPU保存忙碌呢,最简单的方法就是运行一个循环,让CPU不停地跑。

不过我们要控制CPU运行的时间,最简单的方法是调用系统的计时相关的函数进行控制。这里使用的是GetTickCount函数,它会返回从操作系统启动到现在所经过的毫秒数。首先我们使用GetTickCount得到循环前的时间startTime,然后一直循环,直到当前的GetTickCount跟startTime间隔的时间为我们期望的时间才停止循环。这样就达到了控制CPU保持一定时间的忙碌的效果。

怎样让CPU保存空闲呢,这个可以调用系统函数Sleep,让CPU睡了一会,睡着了自然就不做事情了。而且使用Sleep函数可以很方便地控制空闲的时间,因为它自带一个持续时间的参数(以毫秒为单位),我们只要把想要的时间作为参数调用Sleep就可以了。

现在我们可以自由地控制CPU的占用率了。然后要考虑的问题是怎么得到一个正弦曲线。

在任务管理器上我们不可能画出一条完全光滑的正弦曲线,因为它是隔一段时间才刷新一次,相当于它是一个一个的点连接起来得到的曲线。于是我们也是把正弦曲线划分成若干个(COUNT个)点,让CPU的占用率按照这COUNT个点的数值依次出现,然后连成的曲线即为正弦曲线。

不过这COUNT个点的数值是多少呢?显然这个可以根据正弦曲线直接计算出来。正弦曲线的峰值为100%的占用率,谷底为0%的占用率,中间值为50%的占用率,振幅也是50%的占用率,于是第i个点的占用率为:50% + sin(2*PI*/COUNT*i)*50%。即把一个周期2*PI分成COUNT份,第i份代表的弧度即为2*PI/COUNT*i,然后计算正弦值,再调整为中值为50%,振幅为50%的曲线形式。

综合上述两点,我们使用两个数组busySpan[COUNT]和idleSpan[COUNT]表示在每一个点对应的一段时间内CPU需要保持忙碌的时间和空闲的时间。把这一段时间用INTERVAL表示,则对于第i个点,我们已经算得其CPU占用率应该为50% + sin(2*PI*/COUNT*i)*50%,则其在这段时间内应该保持忙碌的时间busySpan[i]为(50% + sin(2*PI*/COUNT*i)*50%) * INTERVAL,化简为(1 + sin(2 * PI / COUNT * i)) * 0.5 * INTERVAL。则剩余的时间INTERVAL-busySpan[i]即为保持空闲的时间idleSpan[i]。

接下来的事情就是让程序按照忙绿、空闲交替地运行下去,具体参考如下代码实现。

首先程序中使用了Windows的API函数和计算正弦、反余弦的三角函数,需要添加相应的头文件:

#include <windows.h>
#include <math.h>

然后才是真正的逻辑处理部分:

const double PI = acos(-1.0);//圆周率PI值
const int COUNT = 200;		 //一个周期的曲线分成的点数
const int INTERVAL = 300;	 //CPU占用率刷新的时间(毫秒)

void fun()
{
	DWORD busySpan[COUNT]; //忙碌的时间
	DWORD idleSpan[COUNT]; //空闲的时间
	for (int i = 0; i < COUNT; i++) //计算相应的时间
	{
		busySpan[i] = (DWORD)(((1 + sin(2 * PI / COUNT * i)) * 0.5 * INTERVAL));
		idleSpan[i] = INTERVAL - busySpan[i];
	}

	for (int i = 0; ; i = (i + 1) % COUNT)
	{
		DWORD startTime = GetTickCount();
		while((GetTickCount() - startTime) <= busySpan[i]); //忙碌busySpan[i]
		Sleep(idleSpan[i]);									//空闲idleSpan[i]
	}
}

以上的程序在单核CPU中应该可以取得比较理想的效果,但现在的CPU大多是多核,所以还要添加多核CPU的处理。

其实关于多核CPU的处理同样是非常简单,只不过要稍微了解相关的Windows API函数。思想就是让程序新建多个线程,让每个线程在一个单独的CPU上跑以上的程序,然后最终的效果就是每个核都可以画出一个正弦曲线。

如果已知计算机CPU的核数,可以在程序中直接指定。不过这里处于扩展性的考虑,在程序中自动进行判断,从而可以在不同核数的CPU中运行。做法是使用GetSystemInfo函数返回关于当前系统的信息,然后从获取得到的信息中取出计算机的CPU数目。

知道CPU核数之后需要创建相应数目的线程,可以使用CreateThread函数进行,这个函数可能相对较为复杂,其原型为:

HANDLECreateThread(
 LPSECURITY_ATTRIBUTES lpThreadAttributes,	        //SD
 SIZE_T dwStackSize,					//initialstacksize
 LPTHREAD_START_ROUTINE lpStartAddress,		//threadfunction
 LPVOID lpParameter,					//threadargument
 DWORD dwCreationFlags,				//creationoption
 LPDWORD lpThreadId					//threadidentifier
);

其中参数的意义为:

  • lpThreadAttributes:指向SECURITY_ATTRIBUTES型态的结构的指针,我们使用NULL(默认安全性),不可以被子线程继承。
  • dwStackSize:设置初始栈的大小,以字节为单位。我们使用0(使用与调用该函数的线程相同的栈空间大小,Windows会根据需要动态延长堆栈的大小)。
  • lpStartAddress:指向线程函数的指针。我们使用void类型指针,通过LPTHREAD_START_ROUTINE进行类型转换,如(LPTHREAD_START_ROUTINE)MyVoid。
  • lpParameter:向线程函数传递的参数,是一个指向结构的指针。我们使用NULL(不需传递参数)。
  • dwCreationFlags :线程标志。我们使用0(表示创建后立即激活)。
  • lpThreadId:保存新线程的id。

函数成功,返回线程句柄;函数失败返回false。

因为有多个线程,我们先根据CPU核数动态创建相应数量的线程句柄数组handle和线程ID数组threadID,然后用for循环逐个创建线程。

当线程创建成功之后,还需要控制其运行在某个固定的CPU核上,否则将达不到控制CPU占用率的效果。这个可以通过API函数SetThreadAffinityMask完成,其原型为:

DWORD_PTR  SetThreadAffinityMask( 
      HANDLE  hThread,					//handle  to  thread 
      DWORD_PTR  dwThreadAffinityMask	                //thread  affinity  mask 
); 

其中第一个参数为线程句柄,第二个参数为指定的CPU核。

关于第二个参数的使用,假如有四个CPU,那么这个参数的二进制表示中后面的四位表示相应的CPU,哪位为1就表示使用哪个CPU。如0×00000001表示使用CPU1,对应的,0×00000002、0×00000004、0×00000008分表表示使用CPU2、CPU3、CPU4。即使用第i+1个CPU对应的参数为0×01 << i。另外,如果要使用前两个CPU,则对应的参数为0×00000003,其他类似。

到这里为止,程序已经可以产生四个线程并独立地在不同的CPU上运行了。最后一个问题是程序什么时候结束,当然这里每个线程都是无尽的循环,程序要一直运行,知道用户按CTRL+C结束或杀死进程。所以主程序也不能结束,主进程一结束,其创建的线程也就结束了,可以简单地通过一个WaitForSingleObject函数进行处理。

我们通过WaitForSingleObject(handle[0], INFINITE)的形式等待第一个线程,但所有的线程都不会结束,所以主进程也不会结束了。

这段代码写在了主函数之中,具体为:

int main()
{
    SYSTEM_INFO info;  
    GetSystemInfo(&info);		    //获取系统信息
    int CPUNum = info.dwNumberOfProcessors; //得到CPU核数
	
	HANDLE *handle = new HANDLE[CPUNum];  //线程句柄
	DWORD  *threadID = new DWORD[CPUNum]; //线程ID

	for (int i = 0; i < CPUNum; i++)
	{
		if ((handle[i] = CreateThread(NULL, 0, 
				(LPTHREAD_START_ROUTINE)fun, 0, 0, threadID+i)) != NULL)
			SetThreadAffinityMask(handle[i], 0x01 << i); //指定CPU
	}
	WaitForSingleObject(handle[0], INFINITE);
}

三 其他扩展

这里介绍的是正弦曲线的画法,关于其他形式的曲线如矩形波、直线,算法思想是一样的,只要根据该曲线指定的CPU占用率形式计算对应的忙碌和空闲的时间即可。即只需要修改fun函数的前半部分,其余的都不需要改变。

另外可以指定不同的线程调用不同的函数,以达到不同的CPU占用率曲线为不同形状的效果。

上一篇: 下一篇:

我的博客

NoAlGo头像编程这件小事牵扯到太多的知识,很容易知其然而不知其所以然,但真正了不起的程序员对自己程序的每一个字节都了如指掌,要立足基础理论,努力提升自我的专业修养。

站内搜索

最新评论