実行優先度の変更

HSPで作成されたアプリケーションは、C/C++などの言語で作成したものと比べると、同じ処理を実行するのにも多めにCPU時間を消費してしまう傾向にあります。特にゲームのようなリアルタイムな処理を必要とするのではなく、常駐型のアプリケーションやスクリーンセーバーを作る場合には、CPU時間を多く消費して他のアプリケーションに影響を及ぼしてしまうようでは困ります。Windows APIには、アプリケーションプログラムのプライオリティ(実行優先度)を変更するための関数があります。今回はそれを用いてHSPのプライオリティを変更してみましょう。

Windowsのタスクスケジュール

WindowsはマルチタスクOSであり、同時に複数のアプリケーションを実行させることができるということはご存知でしょう。Windowsが持つこのような動作は、プロセススレッドによって実現されています。

プロセス:プログラムの実行単位

Windowsでは、実行中のプログラムのインスタンス(実体)のことをプロセスと呼びます。ある実行可能ファイル(.EXE)を1つ起動させることが1つのプロセスを作成することに対応しています。同じアプリケーションプログラムであっても、それを同時に2つ起動させていれば(例えば「メモ帳」を2つ起動させていれば)、そのプログラムのプロセスは2つ存在することになります。

プロセスが作成される際に、そのプロセスのために4Gバイトの仮想アドレス空間が準備されます。そして、この仮想アドレス空間の中に、実行可能ファイル(.EXE)やDLLのコード、プログラムで使用されるデータが格納されていくことになります。

プロセスの仮想アドレス空間については、以前にメモリアドレスについての解説をしたときに説明しているので、そちらも参照してみてください。→Windowsの仮想アドレス空間

スレッド:CPUの割り当て単位

実際には、プロセスだけではプログラムは動きません。プログラムを実行させるのは、プロセス中に生成されたスレッドと呼ばれるものです。

スレッドとは、Windowsシステムの中でCPUが実行時間を割り当てている最小単位のことです。プロセスの仮想アドレス空間に格納されたプログラムコードは、このスレッドによって実行されていきます。

HSPプログラマには、スレッドという言葉になじみがないかもしれませんね。というのも、HSPは1つのプロセスにスレッドが1つだけ存在するシングルスレッドと呼ばれるアプリケーションプログラムであるために、普段スレッドの存在を意識することがほとんどないからです。これとは対照的に、1つのプロセスに複数のスレッドが存在するようなプログラムも存在します。そのようなプログラムはマルチスレッドと呼ばれます。マルチスレッドプログラムは、いわば1つのプロセスの中で複数のプログラムコードを同時に実行できるプログラムです。残念ながら、HSPではマルチスレッドプログラムを作成することができないので、これについての詳しい説明はしません。

Windows上には多数プロセスがあり、それぞれのプロセスが1つ以上のスレッドを持っているので、Windows上には同時にたくさんのスレッドが存在することになります。それぞれのスレッドは、そのときの状態によって、次のように分類することができます。

アクティブ状態のスレッドとは、実行状態にあるスレッドのことです。Windowsは、実行状態にあるスレッドに順番にCPUの実行時間を割り当てることによって、マルチタスクを実現しています。スレッドに1回に割り当てられるCPU実行時間をタイムスライスと呼びます(クォンタムと呼ばれることもあります)。アクティブ状態にあるスレッドは、その時点でCPUのタイムスライスが与えられている実行中状態のスレッドと、タイムスライスが与えられるのを待っているレディ状態のスレッドとに分けられます。

一方、スレッドが停止状態にあり、タイムスライスが与えられない状態のことをサスペンド状態といいます。この状態は休止状態あるいは待機状態などとも呼ばれます。最も多いサスペンド状態の例は、ウィンドウがメッセージを受け取るのを待機している状態です。リアルタイムに動くゲームや、重い計算処理を行う種類のプログラムでもない限り、多くのGUIアプリケーションは実行時間のほとんどすべてがメッセージ待機状態(すなわちサスペンド状態)であるということができます。

サスペンド状態のスレッドが再開(レジューム)するために必要な何らかの条件を満たすと、例えばメッセージ待機中だったウィンドウがメッセージを受け取ると、そのスレッドは再びアクティブ状態となり、タイムスライスが割り当てられるようになります。

スレッドの実行優先度

Windows上で実行されているすべてのスレッドには、それぞれの優先度が付けられています。Windowsは優先度の最も高い実行状態のスレッドにのみ、タイムスライスを割り当てます。1つでも他のスレッドより優先度の高い実行状態スレッドがある場合は、それより低い優先度のスレッドにタイムスライスが与えられることはないため、それらは実行されなくなってしまいます。

では、優先度の低いスレッドが永遠に実行されないのかというと、そういうことはありません。優先度の高いスレッドは常にアクティブな状態にあるのではなく、サスペンド状態にもなります。例えば、メッセージ待ちの状態で、メッセージが送られていないときには、そのスレッドはサスペンド状態になるのです。そのようにして、高い優先度を持つすべてのスレッドがサスペンド状態になれば、Windowsは次に優先度の高いスレッドを実行させるのです。

しかしこのように考えてくると、「最低優先度のスレッドが実行されることはほとんどないんじゃないのかな?」などと考えてしまうかもしれませんね。ところが、上でも述べたように、ほとんどすべてのスレッドは、その大部分の時間がサスペンド状態にあるのです。そのため、低い優先度を持つスレッドにもタイムスライスは割り当てられ、結局はどのスレッドもちゃんと実行されるのです。ただ優先度の高いスレッドの方が先に実行されるだけなのです。

スレッドの実行優先度の設定

スレッドの優先度の決定は次のように行われます。まず、プロセスごとに優先度クラスというものが割り当てられます。この優先度クラスは、そのプロセスがどの程度頻繁に実行されるのかを判断する基準となるものです。次に、プロセスの各スレッドに、プロセスの優先度クラスに対する相対的な優先度が割り当てられます。実際のスレッドの優先度は、プロセスの優先度クラスと、スレッドの相対優先度の組み合わせによって決定されるのです。

プロセスの優先度クラス

Windowsには、『アイドル』、『通常』、『優先』、『リアルタイム』という、4つの優先度クラスが存在します。

優先度クラス 意味
アイドル システムがアイドル状態のときにだけ実行するプロセス
通常 特別なスケジューリングを必要としない一般的なプロセス
優先 タイムクリティカルなタスクを実行するプロセス
リアルタイム 最も高い優先順位クラスを持つプロセス

アイドルクラスは、システム監視アプリケーション、いわゆる常駐アプリケーションには最適なクラスです。常駐アプリケーションのために、他のアプリケーションの速度が低下してしまってはたまりません。こういった場合に、アイドルクラスを指定します。また、スクリーンセーバーをこの優先度クラスで実行させるのも効果的です。

一般的なアプリケーションのほとんどは通常クラスで実行されています。通常クラスは、実はプロセスがフォアグラウンドの場合とバックグラウンドの場合で、そのスレッドの実行方法が異なります。

Windows 98やWindows Meにおいては、同じ通常クラスでも、フォアグラウンドで実行されているプロセスに属するスレッド(正確には、フォアグラウンドにあるウィンドウを生成したスレッド)の方が、それ以外の通常優先度クラスのプロセスに属するスレッドよりもわずかに優先度が高くなります。ユーザーが別のアプリケーションにフォーカスを移すと、元のアプリケーションはバックグラウンドに切り替わって優先度が下がり、新しいアプリケーションはフォアグラウンドになって優先度が上がるようになっているのです。

一方、Windows 2000やWindows XPなどでは、フォアグラウンドで実行されるプロセスのスレッドに対して、他のスレッドよりも長いタイムスライスを割り当てることで、フォアグラウンドアプリケーションを優先させるように動作しています。

システムの設定により、通常クラスのフォアグラウンドプロセスとバックグラウンドプロセスで、タイムスライスの長さが同じになるようにすることもできます。主にサーバ用のWindowsでは、デフォルトでフォアグラウンドプロセスとバックグラウンドプロセスのタイムスライスが同じになるように設定されています。

優先クラスやリアルタイムクラスは、他のプロセスよりも優先度を高くしたいときに用いるものですが、どうしても必要でない限り、これらを通常のアプリケーションで使うことは避けたほうがいいでしょう。他のアプリケーションは通常クラスであるため、これらの実行に支障をきたしますし、場合によっては他のアプリケーションが動かなくなることもありえます。特に、リアルタイムクラスを指定すべきではありません。マウスやキーボードの管理、[Ctrl]+[Alt]+[Delete]キーによる強制終了に関わるスレッドなどの、Windowsシステムが所有する重要なスレッドはたいていが優先クラスとなっているので、場合によっては強制終了すらできない状態に陥ってしまうかもしれません。

プロセスの優先度クラスの変更

では、実際にプロセスの優先度クラスを変更してみるにはどうしたらよいか説明しましょう。

まず、対象のプロセスを指し示すプロセスハンドルと呼ばれるものを取得する必要があります。プロセスハンドルは、特定のプロセスを識別する値のことです。今回は自分自身のプロセスの優先度クラスを変更するので、自身のプロセスのハンドルを取得する必要があります。これには、kernel32.dllが提供するGetCurrentProcess関数を使います。

HANDLE GetCurrentProcess(VOID);

GetCurrentProcess関数は、戻り値としてプロセスの擬似ハンドルを返します。この擬似ハンドルは、真のプロセスハンドルとは異なるのですが、呼び出しプロセス内であたかも自身のプロセスハンドルであるかのように作用するものです。

真のプロセスハンドルの場合には、ハンドルが不要になるとCloseHandle関数でクローズしなければならないのですが、この擬似ハンドルに限ってはCloseHandle関数でクローズする必要はありません。もしCloseHandle関数を呼び出しても、CloseHandle関数では何もせずにすぐに戻ります。

次に、SetPriorityClass関数を呼び出してプロセスの優先度クラスを変更します。SetPriorityClass関数はkernel32.dllによって提供されており、次のように定義されています。

BOOL SetPriorityClass(
    HANDLE hProcess,    // process handle
    DWORD  fdwPriority  // priority class
);

最初のhProcessパラメータには、対象プロセスハンドルを指定します。自身のプロセスの優先度クラスを変更する場合には、GetCurrentProcess関数で取得されるプロセスの擬似ハンドルを指定することができます。

2番目のfdwPriorityパラメータには、プロセスの優先度クラスをあらわす値を指定します。この値として、以下のいずれかを指定することができます。

定数名 意味
NORMAL_PRIORITY_CLASS 0x00000020 通常クラス
IDLE_PRIORITY_CLASS 0x00000040 アイドルクラス
HIGH_PRIORITY_CLASS 0x00000080 優先クラス
REALTIME_PRIORITY_CLASS 0x00000100 リアルタイムクラス

Windows 2000以降では指定可能なパラメータが拡張され、プロセス優先度クラスに次の値を指定することができるようになっています。

定数名 意味
BELOW_NORMAL_PRIORITY_CLASS 0x00004000 通常クラスよりやや低い
ABOVE_NORMAL_PRIORITY_CLASS 0x00008000 通常クラスよりやや高い

スレッドの相対優先度の変更

スレッドの実行優先度は、基本的にはそのスレッドを持つプロセスの優先度クラスを基準に決定されます。ただし、優先度はスレッドごとに多少増減ができるようになっています。これらは、基準となるプロセスの優先度クラスからの相対的な値として指定されます。

スレッドの相対優先度の変更には、まず、対象のスレッドを指し示すスレッドハンドルを取得する必要があります。プロセスハンドルの場合と同じく、スレッドハンドルは特定のスレッドを識別する値のことです。自分自身のスレッドのハンドルを取得するには、kernel32.dllが提供するGetCurrentThread関数を使います。

HANDLE GetCurrentThread(VOID);

GetCurrentThread関数の返すハンドルもまた擬似ハンドルです。この擬似ハンドルは、この関数を呼び出したスレッド内でのみ、自身のスレッドのハンドルとして作用します。この擬似スレッドハンドルの場合も、ハンドルが不要になってもCloseHandle関数でクローズする必要はありません。

次に、プロセスの優先度クラスに対してのスレッド相対優先度を変更します。これには、kernel32.dllが提供するSetThreadPriority関数を使います。

BOOL SetThreadPriority(
    HANDLE hThread,   // thread handle
    int    nPriority  // relative priority
);

最初のhThreadパラメータには、相対優先度を変更するスレッドのハンドルを指定します。自身のスレッドの相対優先度を変更する場合には、GetCurrentThread関数で取得される擬似スレッドハンドルを指定することができます。

次のnPriorityパラメータには、設定する相対優先度を指定します。相対優先度には、以下の値のいずれかを指定することができます。

定数名 相対優先度
THREAD_PRIORITY_IDLE -15 高い
THREAD_PRIORITY_LOWEST -2
THREAD_PRIORITY_BELOW_NORMAL -1
THREAD_PRIORITY_NORMAL 0 標準
THREAD_PRIORITY_ABOVE_NORMAL 1
THREAD_PRIORITY_HIGHEST 2
THREAD_PRIORITY_TIME_CRITICAL 15 低い

スレッドの優先度の実態

実際のスレッドの優先度は、プロセスの優先度クラスとスレッドの相対優先度の組み合わせによって決定されています。優先度は131までの31段階あります。数字が大きいほど優先度が高く、数字が小さいほど優先度は低くなります。

優先度クラス 相対優先度
-15 -2 -1 0 1 2 15
アイドル 1 2 3 4 5 6 15
通常(バックグラウンド) 1 5 6 7 8 9 15
通常(フォアグラウンド) 1 7 8 9 10 11 15
優先 1 11 12 13 14 15 15
リアルタイム 16 22 23 24 25 26 31

サンプルスクリプト

では、実際にスクリプトを書いてみましょう。今回は優先度を変更するモジュールを作成し、それを利用します。

#module ;-------- 優先度変更モジュール --------------------

#uselib "kernel32.dll"
#cfunc GetCurrentProcess "GetCurrentProcess"
#cfunc GetCurrentThread  "GetCurrentThread"
#func SetPriorityClass  "SetPriorityClass"  int,int
#func SetThreadPriority "SetThreadPriority" int,int

; プロセスの優先度クラス
#const NORMAL_PRIORITY_CLASS       0x00000020
#const IDLE_PRIORITY_CLASS         0x00000040
#const HIGH_PRIORITY_CLASS         0x00000080
#const REALTIME_PRIORITY_CLASS     0x00000100

;======== 優先度変更命令 ========
#deffunc SetPriority int p1, int p2

PriorityClass = NORMAL_PRIORITY_CLASS
if p1 < 0 : priorityClass = IDLE_PRIORITY_CLASS
if p1 > 0 : priorityClass = HIGH_PRIORITY_CLASS

relativePriority = p2

; プロセスの優先度クラスを変更
SetPriorityClass GetCurrentProcess(), priorityClass
if stat == 0 : myerr |= 1

; スレッドの相対優先度を変更
SetThreadPriority GetCurrentThread(), relativePriority
if stat == 0 : myerr |= 2

return myerr

#global ;--------------------------------------------------


; アイドル優先度クラス, THREAD_PRIORITY_LOWEST を指定
SetPriority -1, -2
if stat : dialog "優先度変更失敗" : end

repeat
    repeat 1000
        await 0
        title ""+cnt
    loop
    wait 10
loop

上のスクリプトではSetPriorityというモジュール命令を定義してその中で優先度の変更を行っています。SetPriority命令のパラメータは以下のようになります。

SetPriority p1, p2
p1 : 優先度クラスの指定
-1: アイドルクラス
0 : 通常クラス
1 : 優先クラス
p2 : 相対優先度の指定(-15, -22, 15のいずれか)

上記のスクリプトでは、プロセスの優先度クラスをアイドルに、スレッドの相対優先度を-2 (THREAD_PRIORITY_LOWEST) にしています。この指定は、スレッドの実行優先度としては非常に低いものにあたります。実行させてみただけではあまりわかりませんね。このスクリプトを実行させた状態で何か別のアプリケーション(特にCPU時間を奪うもの)を起動させると、遅くなることが実感できるとおもいます。

Pentium 4 HTなどのHT(ハイパースレッディング)テクノロジCPUやCore 2 DuoなどのマルチコアCPU、あるいは複数CPU搭載環境では、あまり変化を実感できないかもしれません。これらの環境では、複数のスレッドが同時に実行中状態となることが可能であるために、あるスレッドが1つのCPU(コア)を占有し続けたとしても、別のCPU(コア)によって他のスレッドを実行できるようになっているためです。

優先度変更の注意点

HSPでスレッド優先度を通常よりも高く設定する場合には注意が必要です。というのも、APIを使って優先度を高くすると、他のアプリケーションが応答しなくなったり、マウスやキーボードが効かなくなる可能性があるためです。

スレッドの優先度を高くすると、そのスレッドがたびたびサスペンド状態にならない限り、より優先度の低い他のスレッドにCPUのタイムスライスが割り当てられないということは先ほど述べました。HSPの場合、サスペンド状態になるのは、stop命令で停止したとき、および、wait命令やawait命令で0よりも大きい待ち時間が指定されることによってスクリプトの実行が中断されているときです。これらの条件下ではHSPのスレッドはメッセージ待ち状態になっていて、次のメッセージがくるまで(または待ち時間が経過するまで)サスペンド状態となります。したがって、stopで停止しているか、またはループ中にwait命令やawait命令を実行していなければ、長時間、優先度を標準より高くするべきではないということです。これらの命令なしで優先度を上げるのは、長くとも数秒程度で終わる処理にとどめるべきです。

筆者の意見としては、スレッドの優先度を上げるのはお勧めできません。そもそも、優先度を上げたところで、さほど速度の差は出ないはずです。むしろ、優先度を下げるほうの目的で使うべきでしょう。