HSPの実行優先度を変更してみる

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

Windowsのタスクスケジュール

WindowsはマルチタスクOSであり、同時に複数のアプリケーションを実行させることができるということはご存知かと思います。これは、Windowsが各スレッドを管理し、順番に実行させているからにほかなりません。

Windowsは、すべてのアクティブな(実行中の)スレッドにCPUのタイムスライス(実行時間)を割り当て続けます。しかし、アクティブでないスレッド、すなわちサスペンド(休止)状態にあるスレッドにはタイムスライスを割り当てません。もし、サスペンド状態にあるスレッドが何らかの条件を満たして実行を再開(レジューム)すれば、再びタイムスライスが割り当てられるようになります。

例えば、GetMessage関数呼び出しによるウィンドウメッセージ待ちのスレッドはサスペンド状態にあります。このスレッドにはタイムスライスは与えられず、他のスレッドが実行され続けます。しかし、メッセージが送られるとこのスレッドは実行を再開し、再びタイムスライスが割り当てられるようになります。

GetMessage関数によるメッセージ待機については『メッセージの送信とポスト』の項を参照してください。


Windows上で実行されているすべてのスレッドには、それぞれの優先度が付けられています。Windowsは優先度の最も高い実行中(アクティブな)スレッドにのみ、タイムスライスを割り当てます。1つでも他のスレッドより優先度の高い実行中スレッドがある場合は、それより低い優先度のスレッドにタイムスライスが与えられることはないため、それらは実行されません。

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

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


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

プロセスの優先度クラス

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

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

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

一般的なアプリケーションのほとんどは通常クラスで実行されています。通常クラスは、実はプロセスがフォアグラウンドの場合とバックグラウンドの場合でその優先度が異なります。同じ通常クラスでも、フォアグラウンドで実行されているプロセスの方がバックグラウンドで実行されているプロセスよりも優先度は高くなります。ユーザーが別のアプリケーションにフォーカスを移すと、元のアプリケーションはバックグラウンドに切り替わって優先度が下がり、新しいアプリケーションはフォアグラウンドになって優先度が上がるようになっているのです。

実際には、通常クラスのフォアグラウンドプロセスとバックグラウンドプロセスで、実行優先度が同じになるように設定することもできます。

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


では、実際に優先度クラスを変更してみるにはどうしたらよいか説明しましょう。すでに起動されているプロセスの優先度クラスを変更するにはSetPriorityClass関数を使います。

BOOL SetPriorityClass(
    HANDLE hProcess,     // プロセスハンドル
    DWORD  fdwPriority   // 優先度クラス
);

SetPriorityClass関数は、hProcessパラメータで指定されたプロセスの優先度クラスを、fdwPriorityパラメータで指定されたクラスに変更するためのものです。このAPI関数ではプロセスハンドルを指定するようになっているので、自分自身のプロセスの優先度クラスを変更するには自分自身のプロセスハンドルを取得する必要があります。自分自身のプロセスハンドルを取得するにはGetCurrentProcess関数を使います。

HANDLE GetCurrentProcess(VOID);

GetCurrentProcess関数は、プロセスの擬似ハンドルを返すものです。この擬似ハンドルは、GetCurrentProcess関数を呼び出したプロセス内でのみ有効なハンドルのことで、実際のプロセスハンドルとは違うのですが、あたかもプロセスハンドルであるかのように作用するものです。通常、プロセスハンドルが不要になるとCloseHandle関数でクローズしなければならないのですが、この擬似ハンドルに限ってはCloseHandle関数でクローズする必要はありません。もしCloseHandle関数を呼び出しても、CloseHandle関数では何もせずにすぐに戻ります。

GetCurrentProcess関数で取得した擬似ハンドルをそのままSetPriorityClass関数に渡してやれば、自身のプロセスの優先度クラスを変更できます。

スレッドの相対優先度

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

BOOL SetThreadPriority(
    HANDLE hThread,    // スレッドハンドル
    int    nPriority   // 相対優先度
);

SetThreadPriority関数は指定されたスレッドのプロセス優先度クラスに対する相対優先度を変更します。hThread パラメータには相対優先度を変更するスレッドのハンドルを、nPriorityパラメータには設定する相対優先度を指定します。相対優先度には、以下のいずれかを指定することができます。

この関数には対象スレッドのハンドルを指定しなければならないので、自分自身のスレッドの相対優先度を変更するには自分自身のスレッドのハンドルを取得しておく必要があります。自身のスレッドのハンドルを取得するにはGetCurrentThread関数を使います。

HANDLE GetCurrentThread(VOID);

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

GetCurrentThread関数で取得した擬似ハンドルをそのままSetThreadPriority関数に渡してやれば、自身のスレッドの相対優先度を変更できます。

スレッドの優先度の実態

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

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

サンプルスクリプト

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

    #include "llmod.as"

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

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

    ; スレッドの相対優先度
    ; #define THREAD_PRIORITY_IDLE          (-15)
    ; #define THREAD_PRIORITY_LOWEST         (-2)
    ; #define THREAD_PRIORITY_BELOW_NORMAL   (-1)
    ; #define THREAD_PRIORITY_NORMAL           0
    ; #define THREAD_PRIORITY_HIGHEST          1
    ; #define THREAD_PRIORITY_ABOVE_NORMAL     2
    ; #define THREAD_PRIORITY_TIME_CRITICAL   15

    #deffunc SetPriority int, int
    mref p1, 0                    ; プロセスの優先度クラス
    mref p2, 1                    ; スレッドの相対優先度
    mref stt, 64                  ; システム変数 stat

    if p1 == 0 : p1 = 0x00000020  ; 省略時は通常クラス
    myerr = 0

    dllproc "GetCurrentProcess", pm, 0, D_KERNEL
    pm.0 = dllret@                ; プロセスハンドル
    pm.1 = p1                     ; 優先度クラス
    dllproc "SetPriorityClass", pm, 2, D_KERNEL
    if dllret@ == 0 : myerr++

    dllproc "GetCurrentThread", pm, 0, D_KERNEL
    pm.0 = dllret@                ; スレッドハンドル
    pm.1 = p2                     ; 相対優先度
    dllproc "SetThreadPriority", pm, 2, D_KERNEL
    if dllret@ == 0 : myerr += 2

    stt = myerr
    return

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

    SetPriority 0x00000040, -2     ; IDLE_PRIORITY_CLASS, THREAD_PRIORITY_LOWEST
    if stat : dialog "優先度変更失敗" : end
*mainloop
    wait 10
    title ""+i
    i++
    goto *mainloop

上のスクリプトではSetPriorityというモジュール命令を定義してその中で優先度の変更を行なっています。プロセスの優先度クラスをIDLE_PRIORITY_CLASSに、スレッドの相対優先度をTHREAD_PRIORITY_LOWESTにしています。この指定は、スレッドの実行優先度としては非常に低いものにあたります。実行させてみただけではあまりわかりませんね。このスクリプトを実行させた状態で何か別のアプリケーション(特にCPU時間を奪うもの)を起動させると、遅くなります。

HSPの優先度変更の注意点

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

優先度を高くすると、スレッド内でたびたびサスペンド状態にならなければ他のアプリケーションにCPUのタイムスライスが割り当てられないことは先ほど述べました。HSPではサスペンド状態になるのは次の場合です。それは、stop命令またはwait命令によってスクリプトの実行が中断されているときです。この条件下ではHSPのスレッドはメッセージ待ち状態になっていて、次のメッセージがくるまでサスペンド状態となります。すなわち、stopで停止しているか、またはループを用いるスクリプトならばその中でwait命令を実行していなければ優先度を標準より高くしてはならないのです。(一時的に優先度を上げるだけですぐに元に戻すならば問題ないですが。)

もう1つ言っておくと、ループ中にawait命令を用いるの場合には注意が必要です。これは、wait命令とawait命令がまったく異なった手法によって実現されているためです。

wait命令は、Win32 APIの提供するユーザータイマー機能によって実現されている命令です。HSPはWin32 APIのSetTimer関数を呼び出すことでその機能を実現しているのです。この関数はタイマーを作成して、指定時間が経過したらウィンドウにメッセージを送るという機能を持っています。スクリプト中でwait命令が実行されると、HSPはこの関数でタイマーをセットしたあとに、メッセージ待機状態に入ります。そして、タイマーメッセージが送られてくると、再び実行を再開します。このメッセージ待機状態ではスレッドはサスペンド状態となるので、他のアプリケーションにもCPU時間が割り当てられます。

それに対して、await命令は、より細かい時間制御を行なうために、スレッドを停止させることなく、マルチメディアタイマーなどを利用して、指定時間が経過したかどうかを常に監視し続けているのです。もちろん外からのメッセージにも応答する必要がありますから、時間が経過したかどうかと同時にメッセージが送られているかどうかも監視し続けているのです。この場合、メッセージがあるかどうかを確認して、メッセージがあればウィンドウプロシージャを呼び出し、メッセージがなかったらそのまま経過時間の監視処理を続行させるという手段をとっているため、サスペンド状態にならなくなってしまいます。

HSPではメッセージを調べるのにwait命令ではGetMessage関数を、await命令ではPeekMessage関数を使用していることによります。これらのメカニズムについては、『メッセージの送信とポスト』の項の解説とも関連しますので、そちらも参照してください。

では、await命令を使っていたらHSPより低い優先度を持つアプリケーションがほとんど実行されなくなってしまうのかというと、必ずしもそうではありません。await命令には、第2パラメータとしてスリープ間隔を指定するパラメータが存在します。このパラメータをSleep関数に渡すことによって、その時間だけスレッドを強制的にサスペンド状態にすることができるのです。

最も問題となるのは、await命令の第2パラメータに0を指定している場合です。そのようにしてしまうと、await命令実行時にSleep関数が呼び出されることはなく、それゆえに、スレッドがサスペンド状態になることがなくなってしまいます。これは、事実上、より低い優先度を持つアプリケーションがほとんど実行されなくなってしまうということを意味します。すなわち、メインループで第2パラメータがゼロのawaitを使用しているHSPアプリケーションは、たとえ自分自身の優先度を変更していなくとも、優先度を低く設定している他のアプリケーションをほとんど止めてしまっているということになります。(よくよく考えると恐ろしいですね……。)したがって、ゲームなどのリアルタイムな処理を必要としなければ、await命令の第2パラメータにゼロを指定しているループそのものをスクリプトから取り除いたほうがいいと思われます。

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