BCCSkeltonで作ったウィンドウ版「ハノイの塔」、とてもバランスよく適度なスピードで動き、気に入っています。
が、
一つだけ難点が。それは最初から書いていた(注)「ハノイの塔」の処理が長いので場合によってはタイムアウト(「応答なし」エラーで落ちる)してしまうことです。
注:【BCCSkelton】「ハノイの塔」プログラム(3)-設計仕様。特にハノイの塔を実行中に他のウィンドウ(プロセス)へフォーカスを移しちゃうと一発です。
ということで、「ハノイの塔」の処理部分だけマルチスレッドにして処理しようと考えましたが、背景用、マスク用、描画用の3つのビットマップの位置変数の書き込みでミューテックス、クリティカルセクション等を使っても、(特にMove関数部分で)描画位置や背景ビットマップ、描画ビットマップ、マスクビットマップの貼り付けで、↓の通りビミョーな差異が生じてしまいます。(背景ビットマップ、マスクビットマップ、描画ビットマップの座標がずれ、残像が残ってしまいます。)
この理由をいろいろと調べ、実験を行っていたら、WEBで次のような記述を見つけました。
「Windows フォームは、本来アパートメントスレッドであるネイティブな Win32 ウィンドウに基づいているため、シングルスレッドアパートメント (STA) モデルを使用します。STA モデルでは、任意のスレッドでウィンドウを作成できるが、ウィンドウの作成後にスレッドを切り替えることはできない。したがって、ウィンドウに対するすべての関数呼び出しは、そのウィンドウを作成したスレッドで実行される必要があります。」
これを調べていたらMicrosoft Learnでも「GDI は複数のスレッドをサポートしていません。 スレッドごとに個別のデバイス コンテキストと個別のレンダリング コンテキストを使用する必要があります。」という記述を見つけました。
この文章通りであれば、「ウィンドウを作って動かしているスレッド(UIスレッド)でしか描画できず、別スレッドにUIスレッドの描画を引き継ぐことは不可」ということになります。(デバイスコンテキストを別にするといっても、UIスレッドのウィンドウのコントロールを別スレッドで作る、ってダメでしょ?)
となると、「ハノイの塔の石盤移動描画」への対応は、
(1)別スレッドでビットマップの座標を計算し、それをキューに入れてUIスレッドで読みだして描画する。→ただし、こうしてもUIスレッドのウィンドウへの描画時間が長いので、結局タイムアウトする可能性がある。
(2)マルチスレッドを諦めてタイマー割込みで描画する。→ただし、基本的に「上へ移動、左右へ移動、下へ移動」という3つのループを使っているので、3つのフェーズのフラッグに合わせて呼び出す「3つの移動描画関数」に分けることが求められます。
(3)「あー、面倒くさいっ!」ということで、「Pop+Pushだけ」(コンソール版と同じ動作)とか、「Zオーダーの最前ウィンドウに固定して実行」する。(一応動くのですが、マウスを色々と弄ると矢張り落ちますね。また、当初の仕様とはならないので、これではイカサマですね。)
ということで、
ここでドツボにはまりましたぁ!
何らソリューションを見いだせないので、「他人はどうやっているのだろう?」ということでググってみると、
(1)C#ではApplication.DoEvents()というものがあり、これで「応答なし」エラーを回避できる、という反面、下手をするとスレッドが閉じられなくなる危険もあるとのこと。
(2)(実質C#と同じようなMicrosoftのC++/CLIではない)C++ではないのか、と調べるとBCBのApplication->ProcessMessage()関数があることはわかりましたが、これもVCLライブラリーの関数であることと、中で何をやっているのかわからないのでパス。
ただし、↑の(2)に関わる資料を読んでいて、どうも(1)と(2)は「処理の長いループの中に『ウィンドウのメッセージ処理』を入れてやり、メッセージキューが溢れないようにする」対処方法であることが分かりました。
ウィンドウのメッセージ処理は通常↓ののようになるのですが、
//ウィンドウのメッセージループ例
while((bRet = GetMessage( &msg, NULL, 0, 0 )) != 0) {
if (bRet == -1) {
//エラー処理
break;
}
else {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
GetMessage関数は「メッセージキューからフィルターに適合メッセージがあるまで待機する」ので、こっちから積極的に読みに行くPeekMessage関数が、今回の場合は適当であると思われます。(注)
注:GetMessage関数とPeekMessage関数の違いー「PeekMessage 関数を使用すると、長い操作中にメッセージ キューを調べることができます。 PeekMessage は GetMessage 関数に似ています。両方とも、フィルター条件に一致するメッセージがないかメッセージ キューをチェックし、 メッセージを MSG 構造体にコピーします。 2 つの関数の主な違いは、フィルター条件に一致するメッセージがキューに配置されるまで GetMessage が返されないのに対し、 PeekMessage はメッセージがキュー内にあるかどうかに関係なく、すぐに返される点です。」(前出Microsoft Learn)
基本設計として、TowerOfHanoi.hのユーザー定義関数に、
//ユーザー定義関数
bool DoEvent(); //Moveの描画中にポストされたメッセージを処理する関数
を追加し、TowerOfHanoiProc.hの実装部分で、
bool CMyWnd::DoEvent() {
MSG msg;
/* 第2引数がNULLの場合、PeekMessageは現在のスレッドに属する総てのウィンドウのメッセージと、
HWND値がNULLである現在のスレッドのメッセージキュー上のすべてのメッセージを取得するので、
ウィンドウとスレッドの両メッセージが処理される。(出典:Microsoft Learn)*/
if(PeekMessage(&msg, (HWND)NULL, 0, 0, PM_REMOVE)) {
if(msg.message == WM_QUIT) {
PostQuitMessage(0); //キューから取り出してしまったので、もう一度メッセージキューに投函する
return FALSE; //その間に中断処理を行う
}
else { //それ以外のメッセージは処理する
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return TRUE;
}
としてみました。赤字の部分はwhile文にしてポストされたメッセージを総て処理することもできますが、「上へ移動、左右へ移動、下へ移動」の3つのループに沿って一つづつ処理をしていくくらいのスピード感で大丈夫だろうと考え、「ポストされたメッセージを一つ読むだけ」のif文にしてみましたがバランス的には良いようです。
また、石盤移動描画処理中にその処理に影響のある割込みがなされると厄介なことになりますので、石盤段数を決定するコンボボックス(IDC_INIT)とハノイの塔を開始するボタン(IDC_START)は共に使用不可にし、「ハノイの塔」完了時に使用可に戻すようにします。
bool CMyWnd::OnStart() {
//戻り値変数
bool ret = TRUE;
//段数選択状態の確認
int num = SendItemMsg(IDC_INIT, CB_GETCURSEL, 0, (LPARAM)0);
//未選択の場合のエラー処理
if(num == CB_ERR) {
MessageBox(m_hWnd, "段数が選択されていません。", "エラー", MB_OK | MB_ICONERROR);
ret = FALSE;
}
else {
//「ハノイの塔」関連コントロールを使用不可にする(「終了」だけ割込み可)
EnableWindow(GetDlgItem(m_hWnd, IDC_INIT), FALSE);
EnableWindow(GetDlgItem(m_hWnd, IDC_START), FALSE);
TowerofHanoi(m_Tire, 0, 1, 2);
MessageBox(m_hWnd, "「ハノイの塔」を終了しました。", "終了メッセージ", MB_OK | MB_ICONINFORMATION);
//描画済の石盤を消す
for(int i = 0; i < m_Tire; i++)
m_Disk[i].Erase();
//柱情報の初期化
for(int i = 0; i < 3; i++)
m_Pole[i].Init();
SendItemMsg(IDC_INIT, CB_SETCURSEL, -1, (LPARAM)0); //未選択状態にする
//「ハノイの塔」関連コントロールを使用可にする
EnableWindow(GetDlgItem(m_hWnd, IDC_START), TRUE);
EnableWindow(GetDlgItem(m_hWnd, IDC_INIT), TRUE);
}
return ret;
}
この状態で考えられる割込みは「終了ボタン」またはシステムボタン(「X」)による終了なので、これに対処しないとなりません。↑のDoEvent関数では「WM_QUIT」メッセージの際(オレンジ文字部分)にキューから取り出したWM_QUITも再度ポストし、BOOLのFALSEを返すことで「WM_QUIT」を拾ったことを呼び出し側に伝えます。
呼び出し側のMove関数は、「上へ移動、左右へ移動、下へ移動」の3つのループで、
while(y >= top) { //top迄上に動かす(例)
m_Disk[disk].Erase(); //石盤を消す
UpdateWindow(m_PicBox.m_hWnd);
y--; //y座標を1ピクセル減らす
m_Disk[disk].Show(x, y); //石盤を表示する
if(!DoEvent()) //WM_QUITを受け取ったら処理を中止する
return FALSE;
}
のようにし、更に呼び出し側のTowerofHanoi関数では、
//「ハノイの塔」メイン関数
void CMyWnd::TowerofHanoi(int diskCount, int fromPole, int toPole, int viaPole) {
if(diskCount > 0) {
TowerofHanoi(diskCount - 1, fromPole, viaPole, toP //ハノイの塔を描画する(石盤移動無し)
//int num = Pop(fromPole);
//Push(toPole, num);
//MessageBox(m_hWnd, "In TowerofHanoi", "Check", MB_OK | MB_ICONINFORMATION); //毎回の移動を確認する場合これを使ってください。ole);
//ハノイの塔を描画する(石盤移動)
if(!Move(fromPole, toPole)) //WM_QUITを受けた場合、
return; //石盤移動描画処理を中止する
TowerofHanoi(diskCount - 1, viaPole, toPole, fromPole);
}
}
のように直ぐに処理を中断するようにします。(注)
注:WM_QUITメッセージの際の上記オレンジ部分の対応を取らずに単にDispatch関数処理を行うと、ウィンドウは消えますが、消えた後も(Move関数処理をしていて)プロセスが残ってしまいますので十分に注意してください。プロセスが完全に終了したか否かは、Ctrl+Alt+Delでタスクマネージャーを呼び出すと確認できます。(一応確実に終了することを私の環境で確認はしています。)
一応ここまでで当初の仕様通りの「ウィンドウ版ハノイの塔」が完成しました。(他のウィンドウの下に行っても、バックグラウンドで処理を続け、最小化しても元に戻せば処理を続けて、「応答なし」エラーが出ないようになりました。)更にSDIウィンドウ(CPICBOXではなく、CANVASで表示する)やDLL(コントロール化する)等、更に弄ることもできますが、一応これで「ハノイの塔」はお開きにしましょう。なお、「最終ウィンドウ版ハノイの塔」はBCCForm and BCCSkeltonパッケージのSampleBCCSkeltonに入れておきましたので、コードを詳しく見たい方はそちらを参照してください。
ps. C#、C++のコンソールから結構楽しめたことは確かですが、このマルチスレッド化失敗とソリューションの研究で大分エネルギーを使ったのも事実です。ちょっと休憩が必要ですね。
