- ベストアンサー
マルチスレッドについて
現在”猫でもできる”の87、88章を学んでおります。 まず87章でマルチスレッドの根本的なやり方を学びましたが、いきなり疑問が浮かびました。 _beginthread関数によりスレッドをスタートさせ、この関数で登録した関数内で_endthread関数を実行し終了させていることは分かります。 しかし_beginthread関数で登録した関数に引数を渡す処理がどの部分で行われているのかわかりません。 登録する関数はvoid型で引数はvoid*型でなければいけないことは分かったのですが、プログラムのどこを見てもこの登録した関数に引数を渡す処理が行われていません。 その辺の動作の説明を分かる方でいいのでよろしくお願いします。 そして88章では排他制御のマルチスレッドを行うプログラムの製作を行っているのですが、ちょっとした疑問が浮かびました。 EnterCriticalSection関数、LeaveCriticalSection関数ではさまれたプログラムは排他制御され他からアクセスされない。 この関数はこんな理解で良いんですかね? この理解で行くと、88章で説明していきますが、子ウィンドウを2つ作成しそれぞれのプロシージャ内で排他制御された関数をスレッドとしてスタートしています。 この2つのスレッドの動作についてですが、互いに排他制御関数が記述されているため、動作としてはまず左の子ウィンドウのスレッドが処理されている場合、右の子ウィンドウのスレッドは停止している。そして左の子ウィンドウのスレッドの排他制御が解放されたときに、右の子ウィンドウのスレッドが開始する。 そしてあるとき左の子ウィンドウのクライアントウィンドウ内で右クリックされた場合、その時点で排他制御されたスレッドが終了するのを待ち、終了したらcountを+1する。 こんな動作が行なわれていると理解してよいのでしょうか?
- みんなの回答 (6)
- 専門家の回答
質問者が選んだベストアンサー
- ベストアンサー
ANo.5です。 >「1%の絶妙なタイミング」といっているのは極端に短い実行時間を持つ右スレッドが実行状態のときにクリックされた場合のことをいっているんですよね? その通りです。 とはいえ、カーネルはブラックボックスなので、あてにならない話として受け取っていただければと思います。
その他の回答 (5)
>よってたとえラウンドロビンとしても、クリックしたときに開始されるスレッドはランダムに発生するので、必ず同じ順に実行されることは考えられないと思います。 クリックされていない時に最も確率の高い「状態」は、左スレッドが無駄ループまたは描画をしていて、右スレッドがクリティカルセクション取得待ちをしている状態ですよね? 左スレッドがクリティカルセクションを取得してから解放するまでの時間に対して、右スレッドがクリティカルセクションを取得してから解放するまでの時間が100分の1だとしたら、99%くらいの確率で、その状態でクリックされる事になるはずです。 クリックされたときに作成されたカウントアップスレッドは、すぐに同じクリティカルセクションの取得待ちになるので、右スレッドとカウントアップスレッドは、同じ条件の待ち状態になります。 では、右スレッドとカウントアップスレッドは、どちらが先にクリティカルセクションを取得できるのか?というのが問題になるわけですよね。 クリティカルセクション取得の動作は、最初に極短時間のスピンロックを試みて、ロックできなければ通常のスレッドスケジューリングに切り替えるという事をしています。先の確立を前提とするなら、すでにクリティカルセクションを取得しているスレッドがあるので、ほぼ必ずスピンロックは失敗します。 そして、どちらが先に起きるのか?に戻ると、ラウンドロビンなら先に寝た方が先に起きるはずだというのが前の回答です。絶妙のタイミングというのはもちろん逆の1%の事です。 実際には99%以上の確立なのではないかと想像します。
補足
回答ありがとうございました。 少し難しかったですが、色々と丁寧に説明をいただき本当にありがとうございました。 このスレッドの状態遷移については、キューのようなものを想像すれば良いですね。 そして左スレッドが無駄に時間が長い処理を行っていることを気にかけずに考えてしまいましたorz そうですよね・・・・大半が左スレッドが実行状態で、他のスレッドは待ち状態になりますよね。 キューで考えるなら左から抜けていくとして [左スレッド][右スレッド][][][][] このような形を考え、今左スレッドが実行されているとするとキューの中は [右スレッド][][][][][] になり、次に実行されるスレッドが右スレッドであるのは確かですね。 この状態が大半を占め、その中でクリックされた場合キューの中は [右スレッド][カウントアップスレッド][][][][] このようになりますよね。 そして左スレッドが終了した時、ラウンドロビンを考え、キューの中は [カウントアップスレッド][左スレッド][][][][] このような状態になり、この状態の時には右スレッドが実行状態になっています。 このように考えると確かにカウントアップスレッドの次に来るスレッドは左スレッドということになりますね。 inthefloiさんが「1%の絶妙なタイミング」といっているのは極端に短い実行時間を持つ右スレッドが実行状態のときにクリックされた場合のことをいっているんですよね?
>加算するスレッドの終了直後左の子ウィンドウのスレッドが必ず開始される Windowsのスレッドは同一優先度ならラウンドロビンでスケジューリングされるはずなので、必ず同じ順番になってしまう事はありえます。 絶妙なタイミングで加算スレッドが起動されれば話は別ですけど。
補足
回答ありがとうございます。 プログラム実行時最初にラウンドロビンでスケジューリングされているのは左の子ウィンドウのスレッドと右の子ウィンドウのスレッドなのでスレッドの開始順序がそのあとのスレッドの実行順序を決定することはありえますが、しかしクリックしたときに発生するスレッドはラウンドロビンでも同じ順番とはならないんじゃないでしょうか? 「左の子ウィンドウのスレッド → 右の子ウィンドウのスレッド」 このように、ある時点でラウンドロビンから予想される次のスレッドが右の子ウィンドウのスレッドで、今現在実行状態になっているのは左の子ウィンドウのスレッドとした場合、 この時点でクリックされた場合左の子ウィンドウのスレッドが終了しだい、countを+1するスレッドが実行され、そのスレッドが終了すると右の子ウィンドウのスレッドが実行状態になるような状況が考えられます。 よってたとえラウンドロビンとしても、クリックしたときに開始されるスレッドはランダムに発生するので、必ず同じ順に実行されることは考えられないと思います。 絶妙なタイミングで加算スレッドが起動されれば話は別とアドバイスをいただきましたが、クリックした時に実行されるスレッドの次に実行されるスレッドが必ず左の子ウィンドウのスレッドになることもかなり絶妙なタイミングだともいえます。 やはりこれは難しい問題なのでしょうか? 確かにこの2つのスレッドは1つの共有変数にアクセスはしていますが、スレッドの開始命令をだしているのはそれぞれ別々のプロシージャで、左の子ウィンドウのクライアントウィンドウでクリックしたときのみ、countを+1加算するスレッドが実行されます。この辺にヒントがあるように感じるのですが・・・・
- 1839cc
- ベストアンサー率54% (12/22)
WaitForSingleObjectによってmutexを所有する処理も行っています。 もし、WaitForSingleObjectが単純に待機するためだけの関数だとすれば、待機から抜けて所有権を取得するまでの間に、他スレッドが所有権を取得してしまうかもしれません。 ですので、待機と所有権の取得は、ひとつのシステムコールで終了する必要があるのです。 APIのネーミングが不親切なのですが、このAPIはかなり汎用的なので、Waitというネーミングが適切だったようです。
補足
回答ありがとうございます。 すいません ちょっと長くなるので補足の部分に記述させてもらいました なるほど、WaitForSingleObject関数は待機と所有する処理を行っているのですね。確かに待機と所有を1つの処理で行わなければ、ある意味所有権を違うスレッドにずっと取られっ放しということだってあるかもしれないですからね。 しかし申し訳ないのですが、またまた疑問が浮かんできました。 あるいみ"mutex"と"CriticalSection"は同じ処理が目的のように考えられるのですが、CriticalSection関数についての疑問です。 88章では3つのスレッドを作成し、そのスレッドごとにCriticalSection関数を記述しているため、ある時点で実行しているスレッドは必ずひとつということになります。実際この章のプログラムを作成すると、クリック時には2つのウィンドウにクリック回数が誤差なく表示されますが、ここで疑問が浮かびました。 実際細かい動作を書くと、左の子ウィンドウのクライアント領域をクリックした場合に、2つの子ウィンドウのクライアント領域にクリック回数が表示されるのですが、左の子ウィンドウのスレッドではCriticalSectionに入ってから多少時間がかかる処理をわざと行い、その後に共通変数countへアクセスし、回数を表示しています。これはきちんと同期が行われているかを確かめるために行っているのですが、これはあくまでクリックされてからスレッドの共通変数countを+1加算するスレッドが終了した直後に実行されれば、確かにクリックした後に多少時間がかかってから表示が更新されますが、共通変数を+1加算するスレッドの直後に、左の子ウィンドウのスレッドではなく、右の子ウィンドウのスレッドが来るということも考えられないでしょうか? 長くなってしまって申し訳ないですが、まとめると このプログラムを実行するとクリックした後、多少時間がかかってから2つの子ウィンドウの表示が更新されます。これはクリックした後に共通変数countを+1加算するスレッドが開始され、そのスレッド終了直後左の子ウィンドウのスレッドが開始されるためです。しかし、自分ではこの処理が納得できず、共通変数countを+1加算するスレッドの終了直後右の子ウィンドウのスレッドが開始される場合があるのではないだろうかと考えています。 もしこの考えのように共通変数countを+1加算するスレッドの終了直後、右の子ウィンドウのスレッドが開始された場合、表示の更新処理にたいして同期することができないのではないかと考えています。しかし、プログラムを実行した場合かならず同期されて表示が更新されます。 これは自分の考えが間違えているからでしょうが、自分の考えのどの部分が違うのかが分かりません。 まぁ単にこの疑問は「共通変数countを+1加算するスレッドの終了直後左の子ウィンドウのスレッドが必ず開始される」とすれば納得できるのですが、どうしてもこんな確約はできないと思うのですが・・・・・ アドバイスをよろしくお願いします。
- Oh-Orange
- ベストアンサー率63% (854/1345)
★引数を渡す処理は WndProc の『WM_CREATE』にあります。 ・そのまま『_beginthread( ThreadMain, 0, &val );』と記述されている『&val』が引数です。 この構造体は、第87章ページの上のほうにある構造体です。→自分で定義したもの。 ウィンドウプロシージャ『WndProc』の定義のすぐ下にも >最後の引数はスレッドに渡されるデータのアドレスです。 という解説があります。→よく読むと見つかりましたよ。 ・なお、_beginthread 関数でスレッド用のコールバック関数『ThreadMain』を登録しているので 以後、ThreadMain() 関数に自分で定義した val 構造体 が void* 型で渡されます。→これはシステムが 渡しますので引数の受け渡しは気にする必要はありません。そのために、_beginthread 関数の第1引数と 第3引数でスレッド処理(コールバック関数)とデータ構造体を関連付けているのです。 ・子ウインドウの場合も同じく『_beginthread( Thread, 0, &val );』の『&val』が引数です。 ただし、val 構造体にセットするウインドウハンドルは、子ウインドウのハンドルです。 これ以外は、メインのプロシージャ『WndProc』にある『WM_CREATE』と同じです。 ●第88章について ・排他制御を行うために EnterCriticalSection 関数~ LeaveCriticalSection 関数でプログラムを 挟むと他のスレッドは『待避状態』となりアクセスできなくなります。これを分かりやすく解説 されているリンクを下に張ります。ここでは『排他制御』のある、なしについて載っています。 http://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AA%E3%83%86%E3%82%A3%E3%82%AB%E3%83%AB%E3%82%BB%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3 ・共通変数に対するスレッドの同時アクセスを防ぐ仕組みですね。つまり、 左ウインドウのスレッドが動作中に 右ウインドウのスレッドが開始されても『待避状態』で停止しています。 そして、左ウインドウのスレッドが終了すると排他制御も解放されるため、 待機中だった右ウインドウのスレッドの処理がここで開始します。 以後、排他制御が解除されるまで別のスレッドが開始しようとしても『待避状態』で待たされます。 最後に: ・第88章については noconan さんの理解されている通りであっています。 複数のスレッドで共通のグローバル変数などにアクセスした場合に、排他制御で同期を取るための処理ですね。 ・あと『猫でもわかるプログラミング』というサイトのリンクなどを貼っておくと知らない人でも適切な アドバイスが出来るようになります。その上で第87章、第88章…と質問して下さい。 ・以上。おわり。
お礼
適切な回答ありがとうございました。 細かい説明のため、疑問が解決されすっきりしました^^ しかも動作が分かりやすく時系列で書かれた排他制御のプロセスが示されたサイトまで紹介いただきありがとうございます。 とても分かりやすかったです。 そしてリンクを貼るのは確かに有効ですね。 これからはそうしようと思います! ついでにちょっと違う質問をしてもよろしいでしょうか? 92章についてです。 この章ではmutexについて行っていますが、またまた疑問が出てきてしまいました。 まず複数のスレッドに対する共通のmutexをメインウィンドウ側で生成します。 スレッド側では初めにWaitForSingleObject(pdata->hmutex, INFINITE); を実行し、スレッドが終了するときに ReleaseMutex(pdata->hmutex); を実行してmutexを解放しています。 具体的な動作はmutexが解放されるまでWaitForSingleObject関数で待機状態にし、mutexが解放されたらWaitForSingleObject関数の記述以降のプログラムが処理されるという動きになると思うのですが、ここで疑問が出てきます。 ReleaseMutex関数によりmutexの解放を行うのはわかるのですが、mutexを所有する処理はどこで行っているのでしょう? もし分かるようならお教えお願いします。
- 1839cc
- ベストアンサー率54% (12/22)
その書籍を持っていないため、一つ目の質問に対してのみの回答をさせていただきます。 スレッドに渡す引数は、_beginthread の第三引数に指定されていませんか? 実際にパラメータとして渡す処理は _beginthread 内部で行われています。
お礼
回答ありがとうございました。 変な質問をしてしまってすみません。 確かに第三引数で指定していましたね・・・・
お礼
返信ありがとうございます。 色々と大変お世話になりました。 また何かしら疑問が発生した場合はお世話になるかもしれませんが、その場合はよろしくお願いします。 失礼します!