首页 > 代码库 > 《编程之美》学习笔记——指挥CPU占用率

《编程之美》学习笔记——指挥CPU占用率

问题:

写一个程序,让用户来决定Windows任务管理器(Task Manager)的CPU占用率(单核)。有以下几种情况:
1.CPU占用率固定在50%,为一条直线
2.CPU的占用率为一条直线,具体占用率由命令行参数决定(范围1~100)
3.CPU的占用率状态为一条正弦曲线
4.多核处理器情况下上述问题怎么解决


分析与解答

首先确定CPU占用率的定义,即在任务管理器的一个刷新周期内,CPU忙(执行应用程序)的时间和刷新周期总时间的比率,就是CPU的占用率,也可以说成,任务管理器中显示的是每个刷新周期内CPU占用率的统计平均值。
所以可以写个程序,在一个刷新周期中,一会儿忙,一会儿闲,调节忙/闲比例,就可以控制CPU占有率了。
一个刷新时间是多久,书上说,通过对任务管理器观测,大约是1秒。鼠标移动、后台程序等都会对曲线造成影响!
单核环境下,空死循环会导致100%的CPU占用率。双核环境下,CPU总占用率大约为50%,四核是25%左右。


解法一:简单解法 

Busy用可循环来实现,for(i=0;i<n;i++) ;
对应的汇编语言为
loop;
mov dx i     ;将i置入dx寄存器
inc dx       ;将dx寄存器加1
mov dx i     ;将dx中的值赋回i
cmp i n      ;比较i和n
j1 loop      ;i小于n时则重复循环

我的cpu是 I5 2410M 2.30GHZ(双核四线程,如图)  因为目前的cpu每个时钟周期可执行两条以上的代码,取平均值2,于是(2300000000*2)/5=920000000(循环/秒) 每秒可以执行循环920000000次。

不能简单的取n=920000000然后sleep(1000),如果让cpu工作1s,休息1s很可能是锯齿,先达到一个峰值然后跌入一个很低的占有率;所以我们睡眠时间改为100ms,100ms比较接近windows的调度时间,n=92000000。如果sleep时间选的太小,会造成线程频繁的唤醒和挂起,无形中增加了内核时间的不确定性因此代码如下:

#include <windows.h>  


int main(void)  
{  
<span style="white-space:pre">	</span>//Run on CPU 0(0x00000001)(00000001)
<span style="white-space:pre">	</span>//Run on CPU 1(0x00000002)(00000010)
<span style="white-space:pre">	</span>//Run on CPU 0 AND CPU 1(0x00000003)(00000101)
<span style="white-space:pre">	</span>//Run on CPU 2(0x00000004)(00000100)
<span style="white-space:pre">	</span>//......
<span style="white-space:pre">	</span>//SetProcessAffinityMask(GetCurrentProcess(),0x1);//进程与指定cpu绑定
<span style="white-space:pre">	</span>SetThreadAffinityMask(GetCurrentThread(), 0x1);//线程与指定cpu绑定
<span style="white-space:pre">	</span>while(true)  
<span style="white-space:pre">	</span>{  
<span style="white-space:pre">		</span>for(int i=0;i<92000000;i++)  
<span style="white-space:pre">			</span>;  
<span style="white-space:pre">		</span>Sleep(100);  
<span style="white-space:pre">	</span>}  
<span style="white-space:pre">	</span>return 0;  
}

使用SetProcessAffinityMask函数,进程与CPU绑定,得到如下图。

使用SetThreadAffinityMask函数,进程与CPU绑定,得到如下图。


解法二:使用GetTickCount()和Sleep()

GetTickCount()可以得到“系统启动到现在”所经历的时间的毫秒值,最多可以统计49.7天,可以利用GetTickCount()判断循环的时间,代码如下:

#include <windows.h>  

const int busyTime=100;  
const int idleTime=busyTime;

int main(void)  
{   
	double startTime;  
	SetProcessAffinityMask(GetCurrentProcess(), 0x1);  
	while(true)  
	{  
		startTime=GetTickCount();  
		while((GetTickCount() - startTime) <= busyTime)  
		{  
			;  
		}  
		Sleep(idleTime);  
	}  
	return 0;  
} 
效果如下图所示,与第一种解法效果差不多,因为都假设当前系统只有当前程序在运行,但实际上,操作系统有很多程序会同时调试执行各种任务,如果此刻进程使用20%的cpu,那我们的程序只有使用30%的cpu才能达到50%的效果。

解法三:能动态适应的解法

使用.Net Framework提供的PerformanceCounter进行查询。具体代码是用C#写的
using System;
using System.Diagnostics; 
namespace CPU
{
    class Program
    {
        static void Main(string[] args)
        {
           cpu(1000);  
        }  
        static void cpu(double level)  
        {  
            PerformanceCounter p = new PerformanceCounter("Processor", "% Processor Time", "_Total");  
            if (p == null)  
            {  
                return;  
            }  
            while (true)  
            {  
                if (p.NextValue() > level)  
                    System.Threading.Thread.Sleep(1000);  
            }  
        }  
    }
}

解法四:正弦曲线

#include <windows.h>  
#include <math.h>  
int main(void)  
{  
	SetProcessAffinityMask(GetCurrentProcess(), 0x1);  
	const double SPLIT=0.01;  
	const int COUNT=200;  
	const double PI=3.14159265;  
	const int INTERVAL=300;  
	DWORD busySpan[COUNT]; //array of busy time  
	DWORD idleSpan[COUNT]; //array of idle time  
	int half=INTERVAL/2;  
	double radian=0.0;  
	for(int i=0;i<COUNT;i++)  
	{  
		busySpan[i]=(DWORD)(half+(sin(PI*radian)*half));  
		idleSpan[i]=INTERVAL-busySpan[i];  
		radian+=SPLIT;  
	}  
	DWORD startTime=0;  
	int j=0;  
	while(true)  
	{  
		j=j%COUNT;  
		startTime=GetTickCount();  
		while((GetTickCount()-startTime)<=busySpan[j])  
			;  
		Sleep(idleSpan[j]);  
		j++;  
	}  
	return 0;  
}  

进一步讨论:

控制CPU占用率,因为要调用Windows的API,要考虑到多核、超线程的情况,要考虑到不同版本的Windows的计时相关的API的精度不同,使问题变得相当复杂,若再考虑其它程序的CPU占用率,则该问题则变得很烦人。(taskmgr调用了一个未公开的API)。对CPU核数的判断,书上是调用GetProcessorInfo,其实可以直接调用GetSystemInfo,SYSTEM_INFO结构的dwNumberOfProcessors成员就是核数。

如果不考虑其它程序的CPU占用情况,可以在每个核上开一个线程,运行指定的函数,实现每个核的CPU占用率相同。如果CPU占用率曲线不是周期性变化,就要对每个t值都要计算一次,否则,可以只计算第一个周期内的各个t值,其它周期的直接取缓存计算结果。

#include<iostream>  
#include<cmath>  
#include<windows.h>  
  
static int PERIOD = 60 * 1000; //周期ms  
const int COUNT = 300;  //一个周期计算次数  
const double GAP_LINEAR = 100;  //线性函数时间间隔100ms  
const double PI = 3.1415926535898; //PI  
const double GAP = (double)PERIOD / COUNT; //周期函数时间间隔  
const double FACTOR = 2 * PI / PERIOD;  //周期函数的系数  
static double Ratio = 0.5;  //线性函数的值 0.5即50%  
static double Max=0.9; //方波函数的最大值  
static double Min=0.1; //方波函数的最小值  
  
typedef double Func(double);  //定义一个函数类型 Func*为函数指针  
typedef void Solve(Func *calc);//定义函数类型,参数为函数指针Func* 

inline DWORD get_time()   
{   
    return GetTickCount(); //操作系统启动到现在所经过的时间ms  
}  

double calc_sin(double x)  //调用周期函数solve_period的参数  
{    
    return (1 + sin(FACTOR * x)) / 2; //y=1/2(1+sin(a*x))  
}  
double calc_fangbo(double x)  //调用周期函数solve_period的参数  
{  
    //方波函数  
    if(x<=PERIOD/2) return Max;  
    else return Min;  
}  
  
void solve_period(Func *calc) //线程函数为周期函数  
{  
    double x = 0.0;  
    double cache[COUNT];  
    for (int i = 0; i < COUNT; ++i, x += GAP)   
        cache[i] = calc(x);   
    int count = 0;  
    while(1)  
    {  
        unsigned ta = get_time();  
        if (count >= COUNT) count = 0;  
        double r = cache[count++];  
        DWORD busy = r * GAP;  
        while(get_time() - ta < busy) {}  
        Sleep(GAP - busy);  
  }  
}  
  
void solve_linear(Func*)  //线程函数为线性函数,参数为空 NULL  
{  
    const unsigned BUSY =  Ratio * GAP_LINEAR;  
    const unsigned IDLE = (1 - Ratio) * GAP_LINEAR;  
    while(1)  
    {  
        unsigned ta = get_time();  
        while(get_time() - ta < BUSY) {}  
        Sleep(IDLE);  
    }  
}  
  
void run(int i=1,double R=0.5,double T=60000,double max=0.9,double min=0.1)  
	//i为输出状态,R为直线函数的值,T为周期函数的周期,max方波最大值,min方波最小值  
{  
	Ratio=R; PERIOD=T; Max=max; Min=min; 
	Func *func[] = {NULL ,calc_sin,calc_fangbo};  //传给Solve的参数,函数指针数组  
	Solve *solve_func[] = { solve_linear, solve_period};  //Solve函数指针数组  
	SYSTEM_INFO info;  
	GetSystemInfo(&info);   //得到cpu数目  
	int NUM_CPUS = info.dwNumberOfProcessors;
	HANDLE *handle = new HANDLE[NUM_CPUS];    
	DWORD *thread_id = new DWORD[NUM_CPUS]; //线程id  

	switch(i)  
	{  
	case 1: //cpu0 ,cpu1都输出直线  
		{  
			for (int i = 0; i < NUM_CPUS; ++i)  
			{  
				Func *calc = func[0];  
				Solve *solve = solve_func[0];  
				if ((handle[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)solve,   
					(VOID*)calc, 0, &thread_id[i])) != NULL)  //创建新线程  
					SetThreadAffinityMask(handle[i], i); //限定线程运行在哪个cpu上  
			}  
			WaitForSingleObject(handle[0],INFINITE);   //等待线程结束  
			break;  
		}  
	case 2: //cpu0直线,cpu1正弦  
		{  
			if ((handle[1] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)solve_func[0],   
				(VOID*)func[0], 0, &thread_id[1])) != NULL)  //创建新线程  
				SetThreadAffinityMask(handle[1], 1); //限定线程运行在哪个cpu上   
			Func *calc = func[1];  
			Solve *solve = solve_func[1];  
			if ((handle[0] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)solve,   
				(VOID*)calc, 0, &thread_id[0])) != NULL)  //创建新线程  
				SetThreadAffinityMask(handle[0], 2); //限定线程运行在哪个cpu上 
			WaitForSingleObject(handle[0],INFINITE);   //等待线程结束  
			break;  
		}  
	case 3: //cpu0直线,cpu1方波  
		{  
			if ((handle[0] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)solve_func[0],   
				(VOID*)func[0], 0, &thread_id[0])) != NULL)  //创建新线程  
				SetThreadAffinityMask(handle[0], 1); //限定线程运行在哪个cpu上  
			Func *calc = func[2];  
			Solve *solve = solve_func[1];  
			if ((handle[1] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)solve,   
				(VOID*)calc, 0, &thread_id[1])) != NULL)  //创建新线程  
				SetThreadAffinityMask(handle[1], 2); //限定线程运行在哪个cpu上  
			WaitForSingleObject(handle[0],INFINITE);   //等待线程结束  
			break;  
		}  
	case 4: //cpu0正弦,cpu1方波  
		{  
			if ((handle[0] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)solve_func[1],   
				(VOID*)func[1], 0, &thread_id[0])) != NULL)  //创建新线程  
				SetThreadAffinityMask(handle[0], 1); //限定线程运行在哪个cpu上  
			Func *calc = func[2];  
			Solve *solve = solve_func[1];  
			if ((handle[1] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)solve,   
				(VOID*)calc, 0, &thread_id[1])) != NULL)  //创建新线程  
				SetThreadAffinityMask(handle[1], 2); //限定线程运行在哪个cpu上  
			WaitForSingleObject(handle[0],INFINITE);   //等待线程结束  
			break;  
		}  
	default: break;  
	}  
}  
  
void main()  
{  
    //run(1,0.5);  //cpu1 ,cpu2都输出50%的直线  
   run(2,0.5,30000); //cpu1 0.5直线,cpu2正弦周期30000  
    //run(3);  //cpu1直线,cpu2方波  
    //run(4,0.8,30000,0.95,0.5); //cpu1正弦,cpu2 0.95-0.5的方波  
}  

这是是第一次CPU跑直线,第二个CPU跑正弦函数~~~~~~~~·




《编程之美》学习笔记——指挥CPU占用率