C# Programming

WPFCPU のクロックスピード

開発環境: Visual Studio 2008

1.目的

最近のプロセッサは、負荷の状態によって、CPUクロックのスピードを調整することができます。最近、eee PC を買ったのですが、実際にどの程度この機能が動いているのかどうか、知りたくなりました。

通常は、Task Managerでプロセッサの状態を知ることができますが、このTask Managerでは実際のプロセッサのクロックを知ることができません。実際のプロセッサのクロックスピードは、WMIを使って知ることができるようなので、試してみました。

2.参考書

  1. C# WMI

3.Windows Forms での実装

 

3.1 クロックスピードを得る方法

CPUのクロックスピードを得るためには、WMIの Win32_Processorクラスを利用して、次のように呼び出します。

Pentium D を使用した場合、CPUは1つしか認識されませんでした。複数のCPUをマザーボード上に搭載している場合は、複数のデータが返される可能性がありますので、注意が必要です。

同様に ExtClock (外部クロック)、MaxClockSpeed(最大クロックスピード)も取得することができます。

なお、WMIを使用する場合には、ソリューションエクスプローラから参照の追加により、System.Managementを追加してあげる必要があります。

この参照設定を行わないと、"型または名前空間名 'ManagementObjectSearcher' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足しています。 "というエラーが発生します。

参照の追加

using System.Management;
...
ManagementObjectSearcher query1 = new ManagementObjectSearcher("SELECT * FROM Win32_Processor");

var res = from ManagementObject item in query1.Get()
select new
  {
    ExtClock = item[
"ExtClock"],
    CurrentClockSpeed = item[
"CurrentClockSpeed"],
    MaxClockSpeed = item[
"MaxClockSpeed"]
  };

Console.WriteLine(res.First().CurrentClockSpeed.ToString());

3.2 タイマーで定期的にデータを取得する

クロックスピードは、CPUの負荷状態により変更される可能性があるため、Timer で定期的にデータを更新する必要があります。

そこで、ツールボックスより TextBox, Label (MHzの表示)タイマーコントロールを追加し、定期的にデータを取得するようにします。Timerのプロパティウィンドウより、Enabled は Trueとし、TimerのIntervalを1000、つまり取得間隔は1秒にしてみました。

Timerの追加

Timerのプロパティ

すると、ManagementObjectの処理に意外と時間がかかり、ウィンドウのリサイズ、移動などの処理ができず、ウィンドウが固まってしまう状態になってしまいました。

対応方法としては、2つあります。

1つは、WMI処理中はカーソルを砂時計にして、処理の間ユーザーに待たせる方法。

もう一つが、バックグラウンド処理にして、別スレッドで処理をすることにより、少なくともウィンドウを動かすことができるようにすることです。

ここでは、バックグラウンド処理で対応することにします。

3.3 バックグラウンド処理

ツールボックスより、BackgroundWorker をフォーム上にドラッグアンドドロップします。

BackgroundWorkerの追加

backgroundWorker1.DoWork += new DoWorkEventHandler(UpdateData);
backgroundWorker1.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(Completed);

private void UpdateData(object sender, DoWorkEventArgs e)
{
  var res = from ManagementObject item in query1.Get()
  select new
    {
      ExtClock = item["ExtClock"],
      CurrentClockSpeed = item["CurrentClockSpeed"],
      MaxClockSpeed = item["MaxClockSpeed"]
    };
  e.Result = res.First().CurrentClockSpeed.ToString();
}

private void Completed(object sender, RunWorkerCompletedEventArgs e)
{
 
uint res = (uint
)e.Result;
  textBox1.Text = res.ToString();
}

なお、バックグラウンド処理では、メインウィンドウの別スレッドで動いているため、直接 TextBoxに値を書くことはできません。無理やりバックグラウンド処理の中でテキストボックスに値を書こうとしても"有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'textBox1' がアクセスされました。"というエラーになります。

このため、Completedイベントハンドラに処理が戻ってきた段階でTextBoxに書き込む必要があります。

しかし、バックグラウンド処理にしても、タイマーが1秒では短すぎて、"BackgroundWorker は現在ビジー状態であるため、複数のタスクを同時に実行できません。"というエラーになってしまいます。そこで、タイマーのインターバルは5秒にしました。 

3.4 ソースコード

以上より、次のようなアプリケーションができます。

using System;
using
System.ComponentModel;
using
System.Linq;
using
System.Windows.Forms;
using
System.Management;
namespace
WindowsFormsApplication14
{
 
public partial class Form1 : Form
  {
   
public Form1()
    {
      InitializeComponent();
     
//
      InitBackgroundWorker();
    }

   
ManagementObjectSearcher query1 = new ManagementObjectSearcher("SELECT * FROM Win32_Processor");

 
  private void InitBackgroundWorker()
    {
      backgroundWorker1.DoWork +=
new DoWorkEventHandler(AsyncGetData);
      backgroundWorker1.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(Completed);
    }

 
  private void timer1_Tick(object sender, EventArgs e)
    {
      if (backgroundWorker1.IsBusy)
       
return
;
      backgroundWorker1.RunWorkerAsync();
    }

 
  private void AsyncGetData(object sender, DoWorkEventArgs e)
    {
   
  var res = from ManagementObject item in query1.Get()
       
select new
        {
          CurrentClockSpeed = item[
"CurrentClockSpeed"]
        };
      e.Result = res.First().CurrentClockSpeed;
    }

 
  private void Completed(object sender, RunWorkerCompletedEventArgs e)
    {
      if (e.Error != null)
      {
         MessageBox.Show(e.Error.Message);
      }
      else if (e.Cancelled)
      {
        MessageBox.Show("Canceled");
      }
      else
     
{
        uint res = (uint)e.Result;
        textBox1.Text = res.ToString();
      }
    }
  }
}

あと少し見た目を変えてあげて、次のような簡単なアプリケーションの出来上がりです。

CPU Speed

なお、Vista では、[コントロール パネル]→[システムとメンテナンス]→[電源オプション]から、お気に入りのプランで、[プロセッサの電源管理]項目から最大のプロセッサの状態を変更することができます。

[省電力]→[プラン設定の変更]を見てみると、規定値で最大で50%となっています。

Vistaの電源オプション

実際に試してみると、50%に落ちました。Task Managerを見てみると、クロックのスピードが半分になったとたんに、CPUの負荷が倍になっているので、実際にクロックが半分になっているようです。

省電力設定

4. WPFに移植する

ところで、Windows Forms はメインテナンスモードに入っています。今後はWPFがメインストリームになるということですね。というわけで、Windows Forms からWPFで作ってみようと思ったら、あれ?タイマーがない、バックグラウンドワーカーがないということに気がつきました。

じゃあどうするの?ということで、ついでにWPFに移植してみることにしました。

4.1 WPFへの移植

単純にコードをWPFのプロジェクトに移植すると、Timer, BackgroundWorkerは、WPFのツールバーの中に存在しません。このため、単純にコードを移植しただけでは動きません。しかし、System.Timers.Timerも、BackgroundWorkerのインスタンスを実装してあげ、WPFでも動きそうです。

しかし、System.Timers.Timer を WPF アプリケーションで使用する場合は、System.Timers.Timer は UI スレッドとは別のスレッドで実行されます。つまり、今回のケースで行くと、バックグラウンドワーカーで処理された結果をGUIに返そうとすると、"このオブジェクトは別のスレッドに所有されているため、呼び出しスレッドはこのオブジェクトにアクセスできません。"という、InvalidOperationException という実行時例外が発生して、動きません。

WPFでは、レンダリング用とUI管理用の2 つのスレッドがあります。通常、開発者が扱うのは UI スレッドだけです。WPF では、ほとんどのオブジェクトが UI スレッドに関連付けられています。したがって、WPFのUI オブジェクトは、そのオブジェクトを作成したUIスレッド上でのみ使用できます。他のスレッドで使用すると、実行時例外が発生します。この点はWindows Formsと同じです。

しかし、System.Timers.Timer で時間が経過した後に呼ばれるイベントハンドラーは、UIスレッド上で呼び出してくれないため、別の方法で対応する必要があります。

WPFでは、このために System.Windows.Timers.DispatcherTimer クラスが用意されています。コードは、ほとんど同じような形で書くことができます。具体的には、

using System.Windows.Threading;

private
DispatcherTimer timer1 = new DispatcherTimer();

timer1.Interval =
new TimeSpan(0,0,5);
timer1.Tick +=
new EventHandler(timer1_Tick);
timer1.Start();

というようになります。

これでWPFでも動作するようになるはずです。

4.2 WPFのソースコード

WPFでのソースコードは次のようになります。

using System;
using
System.Linq;
using
System.Windows;
using
System.ComponentModel;
using
System.Management;
using
System.Windows.Threading;
namespace
WpfCPUClock
{
 
public partial class Window1 : Window
  {
   
public Window1()
    {
      InitializeComponent();
    }

   
private BackgroundWorker backgroundWorker1 = new BackgroundWorker();
   
private DispatcherTimer timer1 = new DispatcherTimer();

   
private void Window_Loaded(object sender, RoutedEventArgs e)
    {
      InitBackgroundWorker();

      timer1.Interval =
new TimeSpan(0,0,5);
      timer1.Tick +=
new EventHandler(timer1_Tick);
      timer1.Start();
    }

   
ManagementObjectSearcher query1 = new ManagementObjectSearcher("SELECT * FROM Win32_Processor");
   
   
private void InitBackgroundWorker()
    {
      backgroundWorker1.DoWork +=
new DoWorkEventHandler(AsyncGetData);
      backgroundWorker1.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(Completed);
    }

   
private void timer1_Tick(object sender, EventArgs e)
    {
      if (backgroundWorker1.IsBusy)
        return
;
      backgroundWorker1.RunWorkerAsync();
    }

   
private void AsyncGetData(object sender, DoWorkEventArgs e)
    {
     
var res = from ManagementObject item in query1.Get()
       
select new
        {
          CurrentClockSpeed = item[
"CurrentClockSpeed"]
        };
      e.Result = res.First().CurrentClockSpeed;
    }

   
private void Completed(object sender, RunWorkerCompletedEventArgs e)
    {
     
if (e.Error != null)
      {
        
MessageBox.Show(e.Error.Message);
      }
     
else if (e.Cancelled)
      {
       
MessageBox.Show("Canceled");
      }
     
else
     
{
       
uint res = (uint)e.Result;
       label1.Content = res.ToString() +
" MHz";
      }
    }
  }
}

 

4.3 バイナリー

バイナリーは次のとおりです。

WpfCPUClock.exe

WPF CPU Clock

eee PC

eee PC 983MHz

あっ、なまけてる!って感じがいいですね。

ただし、CPU-Zとの動きと比較してみると、WMI の返す値があまり正確ではないようなので、数字は目安程度に考えたほうがよさそうです。CPU-Zは、どうやってデータを取っているんだろう・・・