【编程之美】游戏之乐——让CPU占用率曲线听你指挥

让CPU占用率曲线听你指挥


问题描述:写一个程序,让用户来决定Windows任务管理器的CPU占用率曲线。程序约精简越好,计算机语言不限。例如,可以实现下面三种情况:

1.CPU的占用率固定在50%,为一条直线;

2.CPU的占用率为一条直线,具体占用率由命令行参数决定(参数范围1~·100);

3.CPU的占用率状态是一条正弦曲线。


理解要点:当系统中的程序或者在等待用户的输入,或者在等待某些事件的发生,或者主动进入休眠状态,这些程序就会“闲下来”。

在任务管理器的一个刷新周期内,CPU忙(执行应用程序)的时间和刷新周期总时间的比率,就是CPU的占用率。也就是说,任务管理器中显示的是每个刷新周期内CPU占用率的统计平均值。

要操纵CPU的使用率曲线,就需要使CPU在一段时间内跑busy和idle两个不同的循环(loop),从而通过不同的时间比例,来调节CPU使用率。

Busy loop可以通过执行空循环来实现,idle可以通过Sleep()来实现。

问题的关键在于如何控制两个loop的时间。

单核环境下,空死循环会导致100%的CPU占有率。双核环境下,CPU总占有率大约为50%,四核会不会是25%左右呢?

我的CPU是2.6GHZ(双核),现代CPU每个时钟周期可执行两条以上的代码,取平均值2,于是
(2 600 000 000*2)/5=1,040,000,000(循环/秒),也就是说CPU每秒可以执行空循环1,040,000,000次
不过不能简单的取n=1,040,000,000然后Sleep(1000),如果让CPU工作1s,休息1s很可能是锯齿,先达到一个峰值然后跌入一个很低的占有率。所以我们将睡眠时间改为10ms,10ms比较接近windows的调度时间,n=10400000。如果Sleep时间选的太小,会造成线程频繁的唤醒和挂起,无形中增加了内核时间的不确定性。因此代码如下:

// C_Demo.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <windows.h>
int main()
{
	for (;;)
	{
		for (int i = 0; i < 10400000; i++)
			;
		Sleep(128);
	}
	return 0;
}

我们将睡眠时间修改为128秒后,大致得到了一条稳定的50%CPU占用率曲线。

使用这种方法要注意两点:

1.尽量减少sleep/awake的频率,以减少操作系统内核调度程序的干扰;

2.尽量不要调用system call.。因为它也会导致很多不可控的内核运行时间。

显然,这种方法不能适应机器差异性。如何动态了解CPU的运算能力,然后自动调节忙/闲的时间比呢?

使用GetTickCount()和Sleep()

GetTickCount()可以得到“系统启动到现在”所经历时间的毫秒值,最多能够统计到49.7天。此我们可以利用GetTickCount()判断busy loop要循环多久,如下:

#include <windows.h>
int main(void) {
	int busyTime = 128;
	int idleTime = busyTime;

	INT64 startTime = 0;
	SetThreadAffinityMask(GetCurrentProcess(), 0x00000001);
	while (true)
	{
		DWORD startTime = GetTickCount64();
		while ((GetTickCount64() - startTime) <= busyTime)
			;
		Sleep(idleTime);
	}
	return 0;
}

上面两种解法都是假设当前系统只有当前程序在运行,但实际上,操作系统有很多程序会同时调试执行各种任务,如果此刻进程使用10%的cpu,那我们的程序只有使用40%的cpu才能达到50%的效果。
Perfmon.exe是从WIN NT开始就包含在windows管理工具中的专业检测工具之一。我们可以用程序来查询Perfmon的值,.Net Framework提供了PerformanceCounter这一对象,可以方便的查询当前各种性能数据,包括cpu使用率,因此解法三如下:

能动态适应的解法

using System;
using System.Diagnostics;

namespace CPUConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            CPUConsole(0.5);
        }

        private static void CPUConsole(double v)
        {
            PerformanceCounter p = new PerformanceCounter("Processor", "%Processor Time", "_Total");
            if (p == null)
            {
                return;
            }
            while (true)
            {
                if (p.NextValue() > v)
                    System.Threading.Thread.Sleep(128);
            }
            throw new NotImplementedException();
        }
    }
}

正弦曲线

#include <windows.h>
#include <math.h>
int main(void)
{
	SetThreadAffinityMask(GetCurrentProcess(), 0x00000001);
	const double SPLIT = 0.01;
	const int SAMPLING_COUNT = 200;
	const double PI = 3.14159265;
	const int TOTAL_AMPLITUDE = 300;
	DWORD busySpan[SAMPLING_COUNT]; //array of busy time
	DWORD idleSpan[SAMPLING_COUNT]; //array of idle time
	int AMPLITUDE = TOTAL_AMPLITUDE / 2;
	double radian = 0.0;
	for (int i = 0; i < SAMPLING_COUNT; i++)
	{
		busySpan[i] = (DWORD)(AMPLITUDE + (sin(PI * radian) * AMPLITUDE));
		//idleSpan[i] = TOTAL_AMPLITUDE - busySpan[i];
		radian += SPLIT;
	}
	DWORD startTime = 0;
	for (int j = 0;; j=(j+1)%SAMPLING_COUNT)
	{
		startTime = GetTickCount();
		while ((GetTickCount()-startTime)<=busySpan[j])

			;
		Sleep(idleSpan[j]);
	}
	return 0;
}

上述程序并没有实现正弦曲线!!!

其中busySpan[i] = (DWORD)(AMPLITUDE + (sin(PI * radian) * AMPLITUDE));  idleSpan[i] = TOTAL_AMPLITUDE - busySpan[i]; 这样保证了占有率=busy/(busy+idle)=(AMPLITUDE +(sin(PI*radian)*AMPLITUDE ))/(2*AMPLITUDE )=(1+sin(PI*radian))/2 在0到100%之间!

多CPU的问题首先需要获得系统的CPU信息。可以使用GetProcessorInfo()获得多处理器的信息,然后指定进程在哪一个处理器上运行。其中指定运行使用的是SetThreadAffinityMask()函数。另外,可以使用RDTSC指令获取当前CPU核心运行周期数。使用CallNtPowerInformation API得到CPU频率,从而将周期数转化为毫秒数。

如果不考虑其它程序的CPU占用情况,可以在每个核上开一个线程,运行指定的函数,实现每个核的CPU占用率相同。
要让CPU的占用率,呈函数 y = calc(t) (0 <= y <= 1, t为时间,单位为ms )分布,只要取间隔很短的一系列点,认为在某个间隔内,y值近似不变。
设间隔值为GAP,显然在指定t值附近的GAP这段时间内,
CPU占用时间为:busy = GAP * calc(t),
CPU空闲时间为:idle = GAP - busy

如果CPU占用率曲线不是周期性变化,就要对每个t值都要计算一次,否则,可以只计算第一个周期内的各个t值,其它周期的直接取缓存计算结果。
以CPU占用率为正弦曲线为例,显然:y = 0.5 * (1 + sin(a * t + b))
其周期T = 2 * PI / a  (PI = 3.1415927),可以指定T值为60s即60000ms,则
 可以确定a值为 2 * PI / T, 若在这60000ms内我们计算200次(c = 200),则GAP值为 T / c = 300ms.也就是说,只要确定了周期和计算次数,其它几个参数也都确定下来。

#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函数指针数组
	const int NUM_CPUS = 2;  //双核,通用的可以用下面GetSystemInfo得到cpu数目
	HANDLE handle[NUM_CPUS];
	DWORD thread_id[NUM_CPUS]; //线程id
	//SYSTEM_INFO info;
	//GetSystemInfo(&info);   //得到cpu数目
	//const int num = info.dwNumberOfProcessors;
	switch (i)
	{
	case 1: //cpu1 ,cpu2都输出直线
	{
		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 + 1); //限定线程运行在哪个cpu上
		}
		WaitForSingleObject(handle[0], INFINITE);   //等待线程结束
		break;
	}
	case 2: //cpu1直线,cpu2正弦
	{
		for (int i = 0; i < NUM_CPUS; ++i)
		{
			Func* calc = func[i];
			Solve* solve = solve_func[i];
			if ((handle[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)solve,
				(VOID*)calc, 0, &thread_id[i])) != NULL)  //创建新线程
				SetThreadAffinityMask(handle[i], i + 1); //限定线程运行在哪个cpu上
		}
		WaitForSingleObject(handle[0], INFINITE);   //等待线程结束
		break;
	}
	case 3: //cpu1直线,cpu2方波
	{

		/*Func *calc = func[0];
		Solve *solve = solve_func[0];*/
		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: //cpu1正弦,cpu2方波
	{
		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的方波
}

总结几个了解当前线程/进程/系统效能的API:

1.Sleep():让当前线程“停”下来

2.WaitForSingleObject():自己停下来,等待某个事件发生

3.GetTickCount()

4.QueryPerformanceFrequency()、QueryPerformanceCounter():访问更高精度的CPU数据

5.timeGetSystemTime():另一个得到高精度时间的方法

6.PerformanceCounter:效能计数器

7:GetProcessorInfo()/SetThreadAffinityMask():帮助解决多核问题

8:GetCPUTickCount():拿到CPU核心运行周期数。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

镰刀韭菜

看在我不断努力的份上,支持我吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值