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が必要かな。