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

[ ホーム ] [ 上へ ] [ マルチスレッド VC++ 2005 MFC 〜 ワーカースレッド ] [ マルチスレッド VC++ 2005 MFC 〜 ユーザインターフェイススレッド その1 ]

・スレッド作成 & 終了

<Win32APIの場合>

スレッドを作成するにはMFCでも最終的にはAPIのCreateThreadを 呼ぶことによって作成されるのでまずはその定義から。

HANDLE CreateThread(
    LPSECURITY_ATTRIBUTES   lpThreadAttributes,  // セキュリティ記述子
    DWORD                     dwStackSize,         // 初期のスタックサイズ
    LPTHREAD_START_ROUTINE lpStartAddress,       // スレッドの機能
    LPVOID                   lpParameter,          // スレッドの引数
    DWORD                    dwCreationFlags,      // 作成オプション
    LPDWORD                  lpThreadId           // スレッド識別子
);

このAPIの定義で注目したいのが第三、四、五引数です。
第三引数はエントリポイントの制御関数へのポインタを指定します。前頁で解説したAfxWinMainに あたる関数ですが、プライマリスレッドの時と異なり制御関数のシグネチャが違います。 スレッド制御関数に指定できる関数は

DWORD WINAPI ThreadProc(
  LPVOID lpParameter   // スレッドのデータ
);

という形式で、引数lpParameterには、CreateThreadの第四引数に指定した値が渡されて セカンダリスレッドで実行されます。第五引数には指定できる値は0とCREATE_SUSPENDEDです。通常、 CreateThreadを発行するとすぐにスレッドが作られて第三引数に指定した制御関数が実行されますが、 CREATE_SUSPENDEDを指定するとスレッドが作成されますが関数の実行は行われないで保留状態になります。 これはスレッドの他の設定、例えば作成されたスレッドの優先順位を設定したいときなどに設定されます。 実際、MFCの実装では あとで出てきます がAfxBeginThread関数内でスレッドを保留して優先順位を設定できる余地を残しています。

作業が終了したら作ったスレッドはきちんと後始末をしないといけません。その手順は次の2つです。

  1. ExitThreadを発行してスレッドを終了させる。
  2. CreateThreadの戻値という形で取得したハンドルをCloseHandleで閉じる。

1については明示的に発行してもいいですが、制御関数が終了すると自動的に呼ばれるので それほど気にする必要がないかもしれません。 (個人的にはExitThreadを呼んだほうがはっきりしていいと思います。)
2.ついては単にハンドルを閉じればいいように思いますが、実践ではその前にスレッドが 終了したことを確認する必要がでてくるかもしれません。この場合はWaitForSingleObjectでスレッドの ハンドルが必要になるので手順には注意が必要です。

スレッドの作成&破棄の基本は上記のようですが、スレッドを作っただけではだめで、 制御関数で前頁のようなウィンドウの作成、メッセージポンプの構築などややこしい仕組みを 実装しないといけません。大変なので、というよりMFCがあるのでそれはやりません。

<MFCの場合>

Win32APIの場合はすべてを実装しなければなりませんが、MFCには便利な?クラスが あります。既に前頁でも紹介していますがCWinThreadです。マルチスレッドプログラミングを しない限りはほとんど見かけないクラスですが、MFCアプリを作成していればCWinAppという形で 必ず使っています。CWinAppは唯一のアプリケーションクラスですが、このクラスはCWinThreadの 派生クラスで、InitInstanceやRunはCWinAppのメンバではなくてCWinTheadのメンバです。 ということは、CWinThreadを派生させればそれだけでスレッドの初期化もメッセージポンプも 基本的な構造は備わっていて本当に必要なプログラムに集中できます。 CWinThreadでのスレッド作成は次のようです。

1.CWinThread派生クラスのインスタンス生成。

ドキュメントには既定の引数なしのコンストラクタしか書かれていませんが、 thrdcore.cpp内のMFCソースファイルにはもうひとつ、制御関数へのポインタとその関数に渡す パラメータを引数としたコンストラクタが定義されています。

CWinThread::CWinThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam)
{
    //・
}

詳しくは後ほど書きますが、ここではドキュメントにない コンストラクタがあることを覚えておいてください。

2.CWinThread::CreateThreadでスレッドを構築

CWinThreadオブジェクトができたら、CreateThreadでスレッドを作成します。その宣言は

BOOL CWinThread::CreateThread(DWORD dwCreateFlags, UINT nStackSize,
                              LPSECURITY_ATTRIBUTES lpSecurityAttrs)
{
    //・
}

となっています。第一引数はAPIの第五引数に相当するスレッド作成時に 保留(サスペンド)するかどうかを指定します。第二引数と第三引数はそれぞれAPIの第二引数、 第一引数に相当してスタックサイズとセキュリティ属性を設定します。このメンバ関数がAPIの CreateThreadのラッパであることは容易に想像できると思いますが、そうするとあれっ? と思われる方もいるでしょう。制御関数へのポインタを設定する引数がありません。 1.でドキュメントにはないけど、CWinThreadのコンストラクタで与えられると思われるかもしれませんが、 実は1.のコンストラクタで指定する関数へのポインタは、作成するスレッドのエントリポイントの 関数ではないです。

CWinThreadでは、つまりMFCでスレッドを作成する場合は エントリポイントの制御関数は決まっていてthrdcore.cpp内に

UINT APIENTRY _AfxThreadEntry(void* pParam)
{
    //・
}

として定義されています。CWinThread::CreateThread内ではこの関数をエントリポイント としてWin32APIのCreateThreadを呼び出しています。(実際はランタイムライブラリの_beginthreadから 呼び出しています)。そして新しいスレッドが作られてそのスレッド内で上記の_AfxThreadEntry関数が 実行されます。この時点ではプライマリスレッドではCWinThread::CreateThreadが、 セカンダリスレッドでは_AfxThreadEntryが実行されていますが、同期オブジェクトを使いながら この2つの実行を制御して、セカンダリスレッドの内容が事実上稼動する前にCWinThread::CreateThreadを 完了させています。

<ワーカースレッド & ユーザーインターフェイススレッド>

いよいよエントリポイント_AfxThreadEntry関数の実行ですが、 ある条件によって2種類の異なった振る舞いをします。それをMicrosoftはワーカースレッドユーザーインターフェイススレッドと称しています。肝心の_AfxThreadEntryの内容は 次のような感じになっています。

UINT APIENTRY _AfxThreadEntry(void* pParam)
{
    //・
    //・
    DWORD nResult = 0;

    //ワーカースレッド
    if (pThread->m_pfnThreadProc != NULL)
        nResult = (*pThread->m_pfnThreadProc)(pThread->m_pThreadParams);
    //ユーザーインターフェイススレッド
    else if (!pThread->InitInstance())
        nResult = pThread->ExitInstance();
    else
        nResult = pThread->Run();

    //・
    AfxEndThread(nResult);
    return 0;
}

これを見ればわかるかと思いますが、ワーカースレッドは、関数へのポインタである CWinThreadのメンバm_pfnThreadProcに値が設定されていたらその関数を実行するタイプです。 もうひとつのタイプはm_pfnThreadProcがNULLの場合で、InitInstanceでウィンドウを作成し、 メッセージポンプRunを備えている通常のタイプです。

(ワーカースレッド)

CWinThreadのC++オブジェクトを作成したときに、関数へのポインタをドキュメントにはない コンストラクタの引数に指定できたことを思い出してください。こちらの引数付のコンストラクタを 使ってCWinThreadオブジェクトを構築した場合、_AfxThreadEntryからこの関数が実行される仕組みに なっています。ですから、この関数が終了されればスレッドが終了する構造は単純なタイプで、 一時的に重い処理が必要な場合等に用いられます。

(ユーザーインターフェイススレッド)

こちらのタイプは前頁で そのしくみを書いているのでこれ以上は解説しませんが、CWinThread::Runを呼んでいるのでWM_QUITが 届くまで_AfxThreadEntryは終了しません。InitInstanceでウィンドウを作成できるのでいろいろな ことができますが、プライマリスレッド内のデータ共有で同期の問題を解決しないといけない場合が ほとんどなので開発はなかなか大変です。もちろん、ワーカースレッドでも同期の問題はありますが、 ユーザーインターフェイススレッドほど複雑ではないです。

<スレッドの終了>

スレッドの終了は_AfxThreadEntryの最後でAfxEndTheadが呼ばれていて、 その中で最終的にAPIのExitThreadが呼ばれているので、ワーカースレッドではほとんど気にする 必要はなくm_pfnThreadProc内で適当な時期に関数を終了させればいいですし、 ユーザーインターフェイスでは作成したウィンドウの破壊と、メッセージループからの脱出 (WM_QUITメッセージの送信)に注意していくことになります。
最後に、スレッドオブジェクトを閉じないといけませんが、CWinThreadのデストラクタで CloseHandleを呼び出しているので、この点もそれほど気にする必要はないです。 (単純にCWinThreadオブジェクトのメモリリークさえ注意すればいいかと思います)

<AfxBeginThreadヘルパー関数>

CWinThreadを使うとかなり楽になりますが、今までに書いたのはスレッドの作成&破壊で もう少しやることがあります。このことを含めてマルチスレッドはかなり大変なのでMSは CreateThreadを含んだ、より簡単な関数を用意しています。それがAfxBeginThread関数です。
CWinThreadだけを使う場合、ワーカースレッド、インターフェイススレッドの使い分けがコンストラクタの オーバーロードによって実現していました。しかし、ドキュメントには引数付のコンストラクタについては 記述がなくソースを見ないとわからない状態でした。この理由がAfxBeginThreadにあります。 この使い分けはAfxBeginThreadのオーバーロードで解決するように推奨しています。この関数を使うと スレッドを作成してCWinThreadオブジェクトへのポインタ取得がさらに楽になり、 CWinThreadコンストラクタを使う必要がなくなるため、ドキュメントには引数付のコンストラクタの 説明がないと思われます。ではデフォルトコンストラクタはなぜあるの?ということになりますが、 これについてはユーザーインターフェイススレッドの場合、スレッドの構築・破棄を繰り返すことも まれにあるみたいでCWinThread派生クラスオブジェクトを使いまわしするためのようです。 (伝聞調でかいているのは私がそういうのを経験してないのであしからず)
最後にワーカースレッド、インターフェイススレッド用のAfxBeginThreadの定義を掲載します。

ワーカースレッド用

CWinThread* AfxBeginThread(
    AFX_THREADPROC         pfnThreadProc,
    LPVOID                  pParam,
    int                     nPriority = THREAD_PRIOSRITY_NORMAL,
    UINT                    nStackSize = 0,
    DWORD                   dwCreateFlags = 0,
    LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL 
);

インターフェイススレッド用

CWinThread* AfxBeginThread(
    CRuntimeClass*         pThreadClass,
    int                      nPriority = THREAD_PRIORITY_NORMAL,
    UINT                     nStackSize = 0,
    DWORD                   dwCreateFlags = 0,
    LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL 
);

APIのCreateThreadとほぼ同じ感覚で引数を指定すればいいので、ワーカースレッド用は 割とわかりやすいと思います。ちょっとわかりにくいのがユーザインターフェイススレッド用です。 特に第一引数ですが、ここにはCWinThreadの派生クラス名(ex. CDerivedThread)を引数として RUNTIME_CLASSマクロを使うと、AfxBeginThread内でCRuntimeClass::CeateObjectでそのオブジェクトが 作られて戻値として返ってきます。そして、CDerivedThreadでInitInstanceをオーバーライドして ウィンドウ作成をしてやれば、制御関数_AfxThreadEntryでメッセージポンプを 開始してくれてウィンドウ処理が可能になります。

<共有データの同期>

ここまでに主にスレッドの作成と破棄について書きましたが、これはほんの入り口でこの先、 共有データの同期スレッドの生存確認など重大な問題が残っています。 共有データの同期についてはプロセス間通信にごく基本的なことを書いたので それもご覧ください。後者については、プリマリスレッドからセカンダリスレッドを作成し、 そのハンドルをプリマリスレッドに保持した状態でプライマリスレッドを終了したりすると セカンダリスレッドのオブジェクトが取り残されるということを指しています。
次にワーカースレッド・ユーザインターフェイススレッドを実際に使って見ます。