◆マルチスレッド - VC++ 2005 MFC

[ ホーム ] [ 上へ ] [ マルチスレッド VC++ 2005 その2 ]

・概要

前回、プロセス間通信について書いていて遅まきながら 気が付きました。順序が逆でした。今回のお題をプロセス間通信について書く前に 書かなければいけませんでした。どの本を見ても当然ながらこちらが先に書かれています。 難易度もスレッド間通信の方が低いでしょうし、マルチスレッドが前提でのプロセス間通信だからだ と思います。実際、プロセス間通信でのコード掲載でもスレッドの 作成をしているので復習も兼ねて書いてみます。
プロセス間通信でスレッドとはプログラム実行の単位で、 その流れは1スレッドで1つだということを書きましたが、もし、プロセスにスレッドが 1つしかなく、そのスレッド内で重い処理をしたらどうなるでしょう?流れは1つなのでその処理中は 他からの入力による処理などはすべて保留状態になります。実際はメッセージキューにメッセージが 溜まっていくのですが、見た目はフリーズしたようになり不味い状態になります。
そこで、新たにスレッドを作れば、流れが2つになるのでそちらで重い処理をさせ、 最初のスレッドはその他の処理をできるようにします。このような1つのプロセス内に複数の スレッドが存在するプログラムをマルチスレッドといいます。ここで マルチスレッドプログラミングをするにはスレッド内のプログラムの流れを把握しないと 話にならないのでまずはそこから。

<プログラムの流れ>

ここでの話はWindows上で動作するすべてのアプリケーションの根幹をなす部分なので 私の稚拙な記述だけでなく、書籍等などでじっくり勉強されることをお勧めします。Windowsを プラットフォームとしたアプリケーションを作る場合、コンソールアプリケーションを除くと ほとんどが1つ以上のウィンドウを持っているので、この場合について書いてみます。

  1. Notepad.exeなどのアプリケーションが実行されると、 OSはまずプロセスを作成してその中でコードが実行できるようにアドレス空間などを作成します。 と同時に1個のスレッド(プライマリスレッド)を作成します。MFCではプライマリスレッドを表す クラスがCWinAppであり、CWinThreadの派生クラスなので、スレッドハンドルや IDなどもCWinAppの派生クラスのインスタンス内に格納されています。
    2つ目以降のスレッド(セカンダリスレッド)をつくる場合は、CWinThreadの派生クラスを作って CWinThread::CreateThreadを実行することになります。 スレッドができたら(ハンドルが取得できたら)、 OSがこのスレッド単位でプログラムの実行を割り当てていきます。
  2. スレッドができたはいいですけどどこからはじまるか わかりせん。と言いたいところですが、プライマリスレッドではWinMain関数と決まっています。 MFCの場合はいくつかの関数の呼び出しがあるのですが、結局AfxWinMain関数です(winmain.cpp内)。 これがいわゆるエントリポイントと言われる関数です。スレッドという立場でみると AfxWinMain関数スレッドの制御関数といえます。
    あとで、スレッドを作成するときにAPIのCreateThreadについて書きますが、第三引数に作成したスレッドで 実行する関数へのポインタを指定するのですが、プライマリスレッドの場合はAfxWinMain関数が それに当たります。
    セカンダリスレッド(プライマリ以外のスレッド)以降では、スレッド作成時にこのエントリポイント となる制御関数を指定しなければならないことを覚えておいてください。
  3. 実行する関数も決まったので制御関数 (プライマリスレッドの場合はAfxWinMain)の最初から実行していくわけですが、その関数が 終了すると自動的にスレッドも終了し(スレッドオブジェクトは別途、明示的に削除する必要があります)、 すべてのスレッドが終了するとプロセスも終了してしまいます。ところが、実際はウィンドウが表示されて スレッドもプロセスも存在し続けています。AfxWinMainの中身がミソで、その最後のほうで無限ループ構造を もっている関数を呼び出しているためです。プログラムを終了するには、ある条件が与えられて 無限ループから抜けます。するとAfxWinMain関数が終了し、その後スレッド、プロセスが終了 (すべてのスレッドが終了していることが条件)します。

<AfxWinMain関数>

言葉で書いていてもなんなので実際がどうなっているのかを模式的に以下に示します。

int AFXAPI AfxWinMain( /*引数リスト・・・・・*/ )
{
    //・
    CWinThread   *pThread = AfxGetThread();   //(A)
    CWinApp      *pApp    =  AfxGetApp();     //(B)
    //・
    //・
    if ( !pApp->InitApplication() )  { /*エラー処理ルーチンへ*/ }

    if ( !pThread->InitInstance() )        //(C)
    {
        /*・
          ・
        メインウィンドウがあれば破壊処理
          ・
        */
        nReturnCode = pThread->ExitInstance();
    }

    nReturnCode = pThread->Run();           //(D)
    //・
    //・
    return nReturnCode;
}

(A)、(B)でアプリケーションのプライマリスレッドを表すCWinThread、 アプリケーションを表すCWinAppへのポインタがそれぞれpThread、pAppに格納されています。 これはAfxGetThread()、AfxGetApp()でインスタンスを作っているわけではなく、AfxWinMain関数が 始まる前に既に作られていてそれを引っ張り出してポインタを代入しているに過ぎません。

スレッドが作られたらエントリポイントの関数が実行されるので そんなオブジェクトが作られる余地がないように思われるかもしれませんが、 1でスレッドが作られてから最初の関数(WinMain)が 始まるまでに実はランタイムの初期化が行われています(crtexec.c)。今回の件とは直接関係無いように 思いますが、私自身もMFCでは始まりがよくわからないためにかなり苦労した経験があるので 少し書いておきます。
エントリポイントの関数が実行されるまでにランタイムでいろいろやっているみたいですが私には 難しくてよくわかりません。ただ注目してほしいことはここでCWinAppの派生クラスの唯一の アプリケーションクラスのインスタンス化が行われていることです。MFCプロジェクトを作成すると アプリケーションクラスCWinAppの派生クラス、例えばCTestAppのような型のオブジェクトがTest.cppに

CTestApp          theApp;

として定義されています。このtheAppがインスタンス化 されています。結論的にはtheAppへのポインタがAfxGetThread()、AfxGetApp()の戻値として AfxWinMain関数内のpThread、pAppに渡されていますが、その流れは次のようです。

ここでtheAppの継承関係を確認します。

CTestApp → CWinApp → CWinThread → CCmdTarget → CObject

theAppがインスタンス化されるときには上記のCObjectから左に順にコンストラクタが 実行されていきます。順番にみていきます

CObject::CObject(){}             //afx.inl内

ここは何もしてません。

CCmdTarget::CCmdTarget()         //cmdtarg.cpp内
{
    //・
    m_pModuleState = & _AFX_BASE_MODULE_STATE型オブジェクト;
    //・
    //・
}

CCmdTargetクラスのm_pModuleStateメンバはmfcxx.dllがロードされたときに作成されたグローバルな _AFX_BASE_MODULE_STATE型(AFX_MODULE_STATE型の派生クラス)インスタンスへのポインタが 代入されます。_AFX_BASE_MODULE_STATE型はいろいろ複雑な仕組みになっていますが、以下のように CWinAppとCWinThreadへのポインタが代入できるようになっています(実際は、CWinThreadへのポインタの 場合は、代入しようとする段階でポインタを受け付ける変数が作成されています)。

CWinApp::CWinApp(LPCTSTR lpszAppName)    //appcore.cpp内
{
    //・
    AFX_MODULE_STATE        *pModuleState = m_pModuleState;
    AFX_MODULE_THREAD_STATE *pThreadState = pModuleState->m_thread;
    ASSERT(AfxGetThread() == NULL);
    pThreadState->m_pCurrentWinThread = this;
    ASSERT(AfxGetThread() == this);
    
    ASSERT(afxCurrentWinApp == NULL);
    pModuleState->m_pCurrentWinApp = this;
    ASSERT(AfxGetApp() == this);
    //・
    //・
}

CWinAppのコンストラクタは分かりやすくするために 少し書き換えていますが、内容的にはこのようになっています。この段階でtheAppへのポインタthisが、 CCmdTargetのコンストラクタで取得した_AFX_BASE_MODULE_STATE型オブジェクトの各メンバに 代入されているのが分かるかと思います。そして、AfxGetThread()、AfxGetApp()がm_pModuleState内の メンバを使ってCWinThread、CWinAppオブジェクトへのポインタを返却していることも理解できます。

これでpThread、pAppがtheAppへのポインタ、つまりCTestAppオブジェクトへのポインタ ということが分かりました。この次にCWinApp::InitApplicationが呼ばれていますが、これは互換性を 維持するためのもので言及しません。
次に(C)でInitInstance()が呼ばれています。気づかれた方も多いでしょうが、プロジェクトを 作成したときにスケルトンで作成されたCTestApp::InitInstance()がここで実行されます (理由はInitInstanceが仮想関数でCTestAppでオーバーライドしているため)。もし、ここでFALSEを 返したら以降を実行しないでAfxWinMainが終了します。アプリケーションウィザードが作成した スケルトンではこのInitInstance内でメインウィンドウ等の作成を行っています。 これは解説するまでもないですね。

最後に(D)でRun()が実行されます。これも仮想関数なので オーバーライドできますがあまり行われません。この関数が先ほど書いた無限ループ構造をもっています。

int CWinThread::Run()
{
    // ・
    for (;;)
    {
        MSG     msg;
        /*アイドル処理*/
            // ・
        do
        {
            if (! ::GetMessage(&msg, NULL, NULL, NULL) )   //(A)
                return ExitInstance();

            ::TranslateMessage(&msg);                      //(B)
            ::DispatchMessage(&msg);                       //(C)
            // ・
            // ・
        } while (::PeekMessage( &msg, NULL, NULL, NULL, PM_NOREMOVE));
    }
}

上記も分かりやすくするために書き換えているのでそのままだと 不具合になります。実際は少々複雑で、アイドル処理があったり、do〜while間はCWinThread::PumpMessage という形で実装されていたりしていますが、内容的にはこのような無限ループ構造になっています。 この無限ループを抜けてRun()を終了させるにはGetMessageがFALSEを返さないといけません。 それは次に書くとして、ExitInstance()が呼ばれて無限ループから 抜けてRun()が終了します。
ExitInstanceもInitInstanceと同様にCWinApp派生クラスで後始末のためによくオーバーライドされるので 分かりやすいでしょう。ExitInstanceで後処理を済ませると、スレッド制御関数であるAfxWinMainが終了し、 最終的にtheAppが解放されたのちスレッドが終了してそれとともにプロセスも終了するというプログラムの 一連の流れがこれでわかったと思います。

<メッセージポンプ>

ここでCWinThread::Run()について少し詳しく書きます。ここの処理内容の中心は メッセージの取得と送信です。メッセージとは一言で言えばOSから送られてくるスレッドや スレッド内のウィンドウに対するお知らせかと思います。お知らせとはウィンドウがアクティブに なったとか、移動したとか、OSのシャットダウンやサスペンド要求がきたなど様々なものがあります。 そのお知らせに対して一つ一つ独自の処理を加えていくことがWindowsプログラミングの基本です。

だんだん本題から離れていきますが、CWinThread::Run()を 理解するために必要なことかと思いますのでしばらくお付き合いください。例えばアクティブに なってないウィンドウがあり、ユーザがそのウィンドウのキャプションバーをクリックしてアクティブに しようとしています。このとき、アクティブになるまでにいろいろなメッセージが発行されるのですが、 最終的にWM_ACTIVATEというメッセージがOSから発行されます。 このメッセージは次のような流れをたどります。

  1. そのウィンドウが所属するスレッドには メッセージキューと呼ばれるメッセージを溜め込む器があるので、WM_ACTIVATEを含めて次々に 溜め込まれていきます。
  2. ここで初めてRunがでてきます。この中で PeekMessageでメッセージキュー内にメッセージがあるか確認します。 (上図では省略しています。)
  3. メッセージキュー内にメッセージがあることが 確認されたら、(A)のGetMessageで古い順にメッセージを取り出して 上図のMSG型の構造体に格納します。
  4. メッセージがmsg変数に格納できたら(B)で TranslateMessageで変換処理をします。何の変換処理かというと、ショートカットキーなどの 処理のために実行します。詳しいことはショートカットキーの処理 で書いているのでそちらをご覧下さい。
  5. メッセージ変換ができたら、(C)のDispatchMessageで いよいよアクティブになるウィンドウに送られます。どのウィンドウに送るか特定できるのか という問題ですが、MSG構造体のメンバに送られる対象ウィンドウのハンドルが格納されているので 送り先に迷うことはないです。「メッセージをウィンドウに送る」という表現をしましたが、 正確には「ウィンドウに関連付けられたウィンドウプロシージャを呼び出す」 といったほうがいいかもしれません。

Runの前にInitInstanceが呼ばれたことを思い出してください。ここでウィンドウを 作成していると書きました。ウィンドウを作成するには最終的にCreateWindowを呼ばないと いけないのですが、その第二引数でウィンドウクラスなるものを指定しないといけないことを 確認してください。これはC++言語などのいわゆるオブジェクト指向における「クラス」という 意味ではなく、Microsoftが定義したウィンドウに関する重要な属性とでもいうものです。 ウィンドウクラスの実体はWNDCLASS構造体で、ウィンドウに関連付ける関数への ポインタを指定するメンバが含まれています。これがウィンドウプロシージャで DispatchMessageで呼ばれる関数です。まあ、MFCではCWndの派生クラスは、ウィンドウクラスは 登録済みですし、メッセージマップ機構のためウィンドウプロシージャを意識することは 少ないのですが・・・。プログラマはウィンドウプロシージャにメッセージの内容によって コードを記述していきます(MFCはメッセージハンドラという形でコーディングします)。

ウィンドウプロシージャでの処理(メッセージハンドラでの処理)が済んだら、 再びPeekMessageでメッセージキューにメッセージがあるか確認して、あれば3.からの処理を 無限に繰り返しています。DispatchMessageの戻り値はLRESULT型でこの値はウィンドウプロシージャの 戻り値が返ってきます。ということは、ウィンドウプロシージャ(メッセージハンドラ)の処理が 重いといつまでたってもDispathMessageから制御が戻らず、メッセージキューに未処理のメッセージが 溜まる一方になっていまいます。最初に書いたようなフリーズしたような 状態になる理由はここにあります。
このように全体としてみるとOSから届いたメッセージを適切なウィンドウに届け続ける機能がRun()には あるので、この関数はメッセージループとかメッセージポンプと言われています。

ところで、無限ループのままではプログラムは終われません。 上図をみると分かりますが、GetMessageがFALSEを返せばループから 抜けられます。その条件はWM_QUITを受取ったときです。ウィンドウの右上のXを押したときなど、 ウィンドウが破壊されるときにPostQuitMessageを呼ぶとWM_QUITメッセージが届けられてループから 抜けてAfxWinMain関数が終了したのち、スレッド・プロセスが終了します。

大きな流れは上記のとおりですが、SendMessageでメッセージを送信した場合は、 送信先スレッドのメッセージキューには収められず、直接ウィンドウプロシージャを読んだり、 送信先がウィンドウ宛ではないPostThreadMessageはウィンドウプロシージャに送られることは ないなど例外があることにも注意してください。

<まとめ>

最後にこれまでのウィンドウズアプリケーションのスレッド内の構造を下図に示します。 先ほども書きましたがウィンドウプロシージャで重い処理が入る場合、制御がDipatchMessageから 戻らないため、メッセージポンプが動かなくなりフリーズ状態に陥ってしまいます。これを防ぐために プロセス内にもうひとつスレッドを作成してその重い処理をさせようというのが今回の本題です。 これまで長々と書いたのは新しいスレッドを作成することは、この構造を作成することであり、 内容の理解は必要不可欠だからです。ということで、次セクション以降で スレッド作成方法を書いていきます。