关于《编程之美》CPU曲线控制例子的探讨

    上个月到福州路“扫”书店,本来想买几本敏捷和TDD的书,确意外发现了《编程之美》这本书。一开始被吸引是因为书名下面的副标题:微软面试。。。。,因为2005年毕业的时候曾有一次失败的微软面试,所以抱着事后看客的心态拿起来翻翻,这一翻就决定立刻买下这本书。从第一章开始,每一章的例子都很吸引我,虽然有一些题目早知道解法,但看到有更巧妙的方法时还是忍不住击节,要是但是面试的时候能先看看这本书就好了,嘿嘿。夸归夸,缺点还是要提的,本书的勘误之多也是出类拔萃的,拿到书时就看到里面夹了一张勘误表,当时没在意,到看的时候就发现不对劲了,上网一查,原来还有一张更长的勘误表,呵呵。

    书中第9页给出了一个能够根据系统性能动态自适应的解法,就是那个MakeUsage()函数,原文代码清单如下:
// C# code
static void MakeUsage(float level)
{
     PerformanceCounter p = new PerformanceCounter("Processor","% Processor Time", "_Total");

     while(true)
     {
          if(p.NextValue() > level)
               System.Threading.Thread.Sleep(10);
     }
}

勘误表中对这个函数也没有修改,只是增加了对p是否有效的判断

 if(p==NULL)
 {
    Return;
 }

书上说上面的解法能够处理各种CPU使用率参数,不过看起来好像不对劲,“_Total”参数表示那个PerformanceCounter p的值应该是当前系统中所有CPU的占用时间,是一个long型整数,如果当前系统idle时间是96%的话,这个PerformanceCounter p的值就应该是4。代码的本意是当CPU占用小于level时通过循环提高CPU占用时间,当CPU占用超过level时,就Sleep(10)。不过从代码上看应该是起不到应有的效果,因为只要一个循环就可能将CPU的占用提高到100,而Sleep(10)时CPU的占用是0,最终的结果应该只在50和100两个使用率上是准确的。我手上没有合适的C#编译器,不过我用C++改写了这个函数:

LPCTSTR CounterName = _T("\\Processor(0)\\% Processor Time");

void MakeUsage(long percent)
{
    if(percent < 0 || percent > 100)
        return;

    CPdhQuery query;
    CPdhCounter counter(CounterName);

    query.OpenH(NULL);
    query.Add(counter);
    PDH_FMT_COUNTERVALUE fv;
    while(true)
    {
        query.Collect();
        counter.GetFormattedValue(PDH_FMT_LONG, fv);

        //_tprintf(_T("cpu time %d\n"), fv.longValue);
        if(fv.longValue > percent)
            Sleep(10);
    }

    query.Remove(counter);
    query.Close();
}

Processor我用了0号processor,因为我写这个代码用的机器只有一个单核CPU。如果改成:

LPCTSTR CounterName = _T("\\Processor(_Total)\\% Processor Time");

效果是一样的。CPdhQuery类和CPdhCounter类用的是PJ Naughter的代码,可以从http://www.naughter.com/下载CPdh类。我测试的结果如果percent是100,则CPU占用可以稳定在100,对于其它值都是稳定在50左右,也就是说percent参数是无效的,不知道C#的结果如何,有时间再试一下。

    另外,所有的例子都是使用Sleep释放CPU占用,但是从 Windows NT(包括2K和XP) 里获得的时间戳(Timestamp),其最高精度为 10 到 15 毫秒,具体是多少视你机器上的硬件时钟决定,可以用下面一段函数查看你的Windows所能提供的时钟精度:
  SYSTEMTIME st;
 
  while(TRUE)
  {
    ::GetSystemTime(&st);
    _tprintf(_T("Current Time : %02d:%02d:%02d.%03d\n"),
              st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);
  }

这是在某台单核处理的机器上输出结果是:

Current Time : 14:57:49.314
Current Time : 14:57:49.314
Current Time : 14:57:49.314
Current Time : 14:57:49.314
Current Time : 14:57:49.324
Current Time : 14:57:49.324
Current Time : 14:57:49.324
Current Time : 14:57:49.324
Current Time : 14:57:49.324
Current Time : 14:57:49.324
Current Time : 14:57:49.324
Current Time : 14:57:49.324
Current Time : 14:57:49.324
Current Time : 14:57:49.324
Current Time : 14:57:49.334
Current Time : 14:57:49.334

可见,不管循环有多快,这个系统能够提供的精度是10ms,也就是系统时间更新频率是10ms一次,(如果是多核处理器,别忘了调用SetProcessAffinityMask)很多API都依赖于这样一个10ms精度的系统时间,包括Sleep函数,所以想要Sleep函数提供ms级别的精度是不现实的。

    书中对多核CPU没有给出实际的例子,不过结合SetThreadAffinityMask和GetProcessorInfo可以很容易实现,就是对每个CPU启动一个线程,然后在线程内调用SetThreadAffinityMask:
#define  CPU_BIT_MASK       0x00000001
DWORD mask = CPU_BIT_MASK < cpu_no;
SetThreadAffinityMask(GetCurrentThread(), mask);

其中cpu_no是CPU编号,从0开始。


posted @ 2008-08-04 23:09  oRbIt  阅读(1186)  评论(0编辑  收藏  举报