UWSCのスレッドを自在に操る
あんまり推奨行為ではない気がしますが、UWSCのスレッドはネイティブスレッドのようなので、自由に操れます。
Suspend/Resumeは必要ならスクリプト的に実装すべきですが、ま、開発中は使えるかも。
AffinityMaskを操作して、使用するCPUを制限することは、演算系のスレッドが複数必要なケースでは役立つ、、、かも。
スレッドとCPUの関連について - じゅんじゅんのきまぐれ
まずはスクリプト
複数コア環境の人は、タスクマネージャーのパフォーマンス表示が面白いと思うので、やってみて!
単コア環境の人は、Suspend/Resumeくらいしか機能しないので、SampleFunc関数の中身のコメントアウトを解除して、ログ出力を確認する方が良いかも。
Thread.uws
OPTION EXPLICIT IFB GET_UWSC_NAME = "Thread.uws" THEN // CPU情報取得 DIM cpus, packs = Thread.GetCpuInfo(cpus) DIM i FOR i = 0 TO LENGTH(cpus) - 1 PRINT "cpu" + i + " core" + cpus[i] NEXT FOR i = 0 TO LENGTH(packs) - 1 PRINT "cpu" + i + " same package core" + packs[i] NEXT // プロセスのAffinityMask取得 Thread.GetAffinityMask(i) PRINT "process affinity mask:0x" + REPLACE(FORMAT(i, 8, -1), " ", "0") // スレッド操作用に選択項目定義 FOR i = 0 TO LENGTH(cpus) - 1 cpus[i] = "cpu" + i NEXT i = LENGTH(cpus) + 3 RESIZE(cpus, i) cpus[i] = "End" cpus[i-1] = "Resume" cpus[i-2] = "Suspend" cpus[i-3] = "free cpu" // スレッド操作。ビジーループなので、タスクマネージャーと見比べてね DIM name = "sample", loop = TRUE Thread.Start(name, "SampleFunc(<#DBL>" + name + "<#DBL>)") WHILE loop i = SLCTBOX(SLCT_STR, 0, "control thread", cpus) SELECT i CASE -1, "End" loop = FALSE CASE "Suspend" PRINT i + ":" + Thread.Suspend(name) // 一時停止:失敗(-1)、成功(前回停止カウンター) CASE "Resume" PRINT i + ":" + Thread.Resume(name) // 再開:失敗(-1)、成功(前回停止カウンター) CASE "free cpu" PRINT i + ":" + Thread.SetAffinityMask(name) // 設定:失敗(0)、成功(前回マスク) DEFAULT i = VAL(COPY(i, 4)) PRINT "cpu" + i + " set:" + Thread.SetAffinityMask(name, Thread.MakeAffinityMask(i)) SELEND WEND Thread.Stop(name) Thread.Dispose() // スクリプトの最後に破棄した方が良いかも ENDIF // スレッド関数サンプル PROCEDURE SampleFunc(name) // Threadにつけた名前は必ず必要 WHILE Thread.IsLoop(name) // 無限ループスレッドは、条件をIsLoopに従う //GETTIME() //PRINT G_TIME_HH2 + ":" + G_TIME_NN2 + ":" + G_TIME_SS2 //SLEEP(1) WEND MSGBOX("Thread Exit?") Thread.Exit(name) // 終了前に必ずExitを呼ぶ FEND MODULE Thread DEF_DLL GetCurrentThreadId(): DWORD: kernel32 DEF_DLL OpenThread(DWORD,BOOL,DWORD): DWORD: kernel32 DEF_DLL CloseHandle(DWORD): BOOL: kernel32 DEF_DLL SuspendThread(DWORD): DWORD: kernel32 DEF_DLL ResumeThread(DWORD): DWORD: kernel32 DEF_DLL GetProcessAffinityMask(DWORD,var DWORD,var DWORD): BOOL: kernel32 DEF_DLL SetProcessAffinityMask(DWORD,DWORD): BOOL: kernel32 DEF_DLL SetThreadAffinityMask(DWORD,DWORD): DWORD: kernel32 HASHTBL _loops HASHTBL _threads DIM _hProcess PROCEDURE Thread DEF_DLL GetCurrentProcess(): DWORD: kernel32 _hProcess = GetCurrentProcess() FEND PROCEDURE Start(name, func, op=EMPTY) _loops[name] = TRUE THREAD PrivateFunc(name, func, op) FEND FUNCTION PrivateFunc(name, func, op) _threads[name] = OpenThread($62, FALSE, GetCurrentThreadId()) RESULT = EVAL(func) FEND FUNCTION Exit(name) RESULT = _threads[name, HASH_EXISTS] IFB RESULT THEN CloseHandle(_threads[name]) RESULT = _threads[name, HASH_REMOVE] ENDIF FEND FUNCTION Stop(name, max=0, bResume=TRUE) _loops[name] = FALSE // 終了待ち DIM res = 1, counter = 0 WHILE _threads[name, HASH_EXISTS] IF res > 0 AND bResume THEN res = this.Resume(name) SLEEP(0.01) IFB max > 0 THEN counter = counter + 1 IF counter > max THEN BREAK ENDIF WEND RESULT = _loops[name, HASH_REMOVE] FEND FUNCTION IsLoop(name) RESULT = FALSE IF _loops[name, HASH_EXISTS] THEN RESULT = _loops[name] FEND FUNCTION Suspend(name) RESULT = -1 IF _threads[name, HASH_EXISTS] THEN RESULT = SuspendThread(_threads[name]) FEND FUNCTION Resume(name) RESULT = -1 IF _threads[name, HASH_EXISTS] THEN RESULT = ResumeThread(_threads[name]) FEND FUNCTION Dispose() RESULT = LENGTH(_threads) DIM i, name FOR i = 0 TO RESULT - 1 name = _threads[i, HASH_KEY] this.Stop(name) NEXT _threads = HASH_REMOVEALL _loops = HASH_REMOVEALL FEND FUNCTION GetAffinityMask(var mask) IFB !GetProcessAffinityMask(_hProcess, mask, RESULT) THEN RESULT = 0 ENDIF FEND FUNCTION SetAffinityMask(name=EMPTY, mask=0) IF mask = 0 THEN this.GetAffinityMask(mask) IFB name = EMPTY THEN RESULT = SetProcessAffinityMask(_hProcess, mask) ELSEIF _threads[name, HASH_EXISTS] THEN RESULT = SetThreadAffinityMask(_threads[name], mask) ELSE RESULT = FALSE ENDIF FEND FUNCTION MakeAffinityMask(no) RESULT = POWER(2, no) FEND FUNCTION GetCpuInfo(var rcpus) DEF_DLL GetLogicalProcessorInformation(var DWORD[], var DWORD): BOOL: kernel32 DIM infoLen = 0, res TRY GetLogicalProcessorInformation(NULL, infoLen) res = (infoLen > 0) EXCEPT res = FALSE ENDTRY HASHTBL cpus HASHTBL cores IFB res THEN DIM infos[infoLen / 4] res = GetLogicalProcessorInformation(infos, infoLen) DIM i, core = 0, no = 0, set FOR i = 1 TO LENGTH(infos) - 6 STEP 6 SELECT infos[i] CASE 0 no = 0 WHILE infos[i-1] IF infos[i-1] MOD 2 THEN cpus[no] = core infos[i-1] = INT(infos[i-1] / 2) no = no + 1 WEND core = core + 1 CASE 1 no = 0 set = -1 WHILE infos[i-1] IFB infos[i-1] MOD 2 THEN IF set = -1 THEN set = no cores[no] = cpus[set] ENDIF infos[i-1] = INT(infos[i-1] / 2) no = no + 1 WEND SELEND NEXT ENDIF IFB LENGTH(cpus) THEN rcpus = SAFEARRAY(0, LENGTH(cpus) - 1) FOR res = 0 TO LENGTH(cpus) - 1 rcpus[res] = cpus[res] NEXT ELSE rcpus = SPLIT(EMPTY) ENDIF IFB LENGTH(cores) THEN RESULT = SAFEARRAY(0, LENGTH(cores) - 1) FOR res = 0 TO LENGTH(cores) - 1 RESULT[res] = cores[res] NEXT ELSE RESULT = SPLIT(EMPTY) ENDIF FEND ENDMODULE
まずは、使ってみます。
実行すると、CPU情報のログが出力されます。
Hyper-threadingの場合、cpu番号とcore番号が1対1ではないと思います。
物理CPUが複数の場合、どのcpuがどのcoreに属するかもわかると思います。
SLCTBOXで、cpuのリストと「free cpu」「Suspend」「Resume」「End」が出ます。
この時すでに裏で、ビジーループが回っているので、タスクマネージャーで確認してください。
1コア環境では面白くないですが、CPU使用率は2コアでは50%、4コアでは25%、、、となっているかと思います。
また、使用CPUに偏りはあるかもしれませんが、100%のCPUはないかと思います。
SLCTBOXで、どれかのcpuを選んでください。
選択したCPUが100%(天井張り付き)になったかと思います。
これが、SetAffinityMaskの効果です。
ビジーループを1CPUに割り当てるので、そのCPUが100%になります。
「free cpu」は、AffinityMaskの設定をプロセスと同じにします。
選択すると、100%CPUはなくなると思います。
「Suspend」は、一時停止です。
CPU使用率が一気に下がると思います。
「Resume」は、一時停止の再開です。
何度も「Suspend」している場合、回数分「Resume」が必要になります。
「End」または×印で、終了です。
使い方
本モジュールをインクルード(CALL)します。
THREAD用関数に若干手を加えます。
THREADの呼び出しと終了周りに手を加えます。
例えば
THREAD ThreadFunc() // メインの処理 // (通常、いろいろあるかと思いますが省略) MSGBOX("End?") PROCEDURE ThreadFunc() WHILE TRUE // スレッドでの処理 SLEEP(0.01) WEND FEND
が、こんな感じです。
CALL Thread // Threadモジュールが必要なので、CALLしてください DIM name = "thread name" // スレッドの名前。なんでも良いです Thread.Start(name, "ThreadFunc(<#DBL>" + name + "<#DBL>)") // スレッドの起動を変えます // メインの処理 // (通常、いろいろあるかと思いますが省略) MSGBOX("End?") Thread.Stop(name) // スレッドの終了。Disposeで全スレッド終了するならなくても良い Thread.Dispose() // スクリプトの最後に破棄した方が良いかも PROCEDURE ThreadFunc(name) // nameが引数に必要 WHILE Thread.IsLoop(name) // 無限ループスレッドは、条件をIsLoopに従う // スレッドでの処理 SLEEP(0.01) WEND Thread.Exit(name) // 終了前に必ずExitを呼ぶ FEND
ThreadFuncに引数が必要なケースは、「op」という変数を第二引数に指定できます。
複数の場合は、そこをSafeArrayにして詰め込むか、Threadモジュールを修正するか。
解説
スレッドについて
基本的な話です。
同じリソースを使わない処理は、コア数まで同時実行が可能です。
クアッドコアのCPUの場合、4つの処理が同時実行可能。
大雑把に言って、独立した4つのスレッドが全力で実行できる、ということ。
クアッドコアCPUで、タスクマネージャーを見たとき、CPUが8つあることも多いと思います。
1つの物理コアが2つの論理CPUになっています。
これが、Hyper-threading。
ある処理をする時、CPUの全体が常に使われるのではなく、あいている部分がある。
ここを有効活用する技術。
複数スレッド(Hyper-threadingの現状の実装では2スレッドのみ)のコアなのです。
例えば、アドレスAとアドレスBから読み込み、演算、アドレスCに書くという処理と
アドレスDから読み込んで、アドレスEに書く、という処理なら、
演算中、メモリーアクセスは暇なので、アドレスDを読み込むことができる、という感じ。
ただ、2つのスレッドでやりたいことが衝突した場合、待たされるので、同じようなことをするスレッドは重ならないほうが良い。
しかし、全く違うことをするとCPUの内部キャッシュ破棄の問題がありそうなので、、、もどかしいところ。
とはいえ、Hyper-threading OFFに比べ20%程度の向上は見られるらしい。
はっきりいえそうなのは、CPUに負荷のかかる処理は、別コアで行うべき、というところ。
あと、パッケージ。
物理CPUを二つ積んでる場合にパッケージで判定できます。
デュアルコアCPU二つと、クアッドコアCPU一つを区別するためのもの。
パッケージも内部キャッシュに関連します。
まとめ。
CPUに負荷のかかる処理は、別コアに割り当てること。
同じメモリーにアクセスする処理は、同じパッケージに割り当てること。
お互い排他しながら同じメモリーにアクセスするスレッド(そんなのあるか?)は、同コアに割り当てること?
Thread.GetCpuInfoについて
XPx64以降かと思ったら、x86でもXP SP3あたりで大丈夫なようです。
(というか、手元に機能しない環境がない)
上のパッケージとかコアとかを判定するためのもの。
そうそう変わるものではないので、いらないかもしれません。
複数のCPUを積んだ環境がなかったため、パッケージが正常に動作するか未確認。
AffinityMaskについて
そのプロセス/スレッドが、どのCPUで動くかを決定するもの。
ビット演算で指定するので、例えばCPU0/2/5で動かしたいなら、2進数100101=37になります。
Thread.GetAffinityMaskは、引数にプロセスのAffinityMaskを返します。
リターンは、0が失敗。成功では、システムのAffinityMaskを返します。
Thread.SetAffinityMaskについて
第一引数にEMPTYを渡すと、プロセスに対する処理に書いてありますが、ま、おまけです。
使用したことありません。
第二引数を省略すると、プロセスのAffinityMaskを使います。
スレッドにAffinityMaskを設定します。
失敗は0、成功は前回AffinityMaskです。
元に戻す場合は、前回値をとっておくと良いですが、通常プロセスのAffinityMaskなので、第二引数省略で呼べばよいと思います。
Thread.uwsをそのまま実行して、何回も設定すると、都度変わる様子がわかると思います。
MSDNに、THREAD_SET_INFORMATION(0x20)が必要と書いてあるので、それを指定していたのですが、騙されました。
英語版を見ると、THREAD_QUERY_INFORMATION(0x40)も必要、とのことです。
どうも、THREAD_SET_LIMITED_INFORMATION(0x400)|THREAD_QUERY_LIMITED_INFORMATION(0x800)でも良いみたい。
Suspend/Resume
一時停止および再開。
Suspendする毎に停止カウンターが増えます。
Suspend失敗は-1、成功は前回停止カウンター。
すなわち、初回Suspendでは0が返ってくる。
Resumeする毎に停止カウンターが減ります。
Resume失敗は-1、成功はこれも前回停止カウンター。
すなわち、1回SuspendしてResumeすると、1が返ってくる。
動いているスレッドをResumeしても問題ない(0が返ってくる)
注意!Suspendは無条件でとめます。
例えば、起動したスレッドがPRINT実行中(ログファイルロック中)に、メイン側でSuspendしPRINTしようとすると、、、
多分、デッドロックする。(メイン側がSuspendしたスレッドのPRINT完了待ちになる)
タイミングがシビアなので、確認はできてませんが、、、。
他にもデッドロックのパターンは考えられるので、注意が必要です。
タスクマネージャーからプロセスを強引に停止できない人は、使わない方が良いです。
ま、確率は低いけど。
最後に
他のプロセスに対して、AffinityMaskを設定したい場合は、タスクマネージャーの「プロセス」タブで「関係の設定」をすると良いと思いますよ。
Vista以降なら、DOSCMDでキックする時、startコマンドを使う手もある。
起動中のプロセスでタスクマネージャーを使いたくないという場合は、SetProcessAffinityMaskが使えますが、OpenProcessが必要かな。