マルチスレッド機能を使うと、ひとつのプロセス内で複数の処理を同時並行的に行うことができます。
たとえば、時間のかかる処理をバックグラウンドで行い、
その間もUIを有効にしてユーザを待たせないようにすることができます。実際、
ViViでは巨大なファイルを読み込む時にマルチスレッドを使用し、
ユーザを待たせないようにしたことで使い勝手が向上したと考えています。
単純なマルチスレッドは簡単に実装できますが、複数のスレッドが単一(または複数)
データにアクセスする時は、いわゆる同期をとる必要があり、
効率とかをちゃんと考えると結構むつかしいものです。しかし、
MFCは同期をとるための機構もいくつか用意しているので、ちゃんと考えてプログラムを組めば、
ちゃんとしたプログラムができあがるはずです。
MFCでマルチスレッドを使用する時は AfxBeginThread を使うのが最も簡単であると思います。
この関数は2種類あります。ひとつはワーカスレッドを生成するもの、
もうひとつはUIスレッドを生成するものです(詳しくはヘルプを参照してね)。
ここでは通常よく使用するワーカスレッド用の、
CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );
の使用例を説明します。
何かのきっかけでスレッドを起動したい場合は、そのための制御関数
UINT MyControllingFunction( LPVOID pParam );
を定義します。引数はそのスレッドを識別するためのデータです。この使い方は後述します。そして、 制御関数を指定して AfxBeginThread をコールすると、スレッドが生成され、 スレッドを起動したスレッドとあわせて2つが同時に実行を開始します。 AfxBeginThread は第3引数で起動するスレッドのプライオリティを指定可能なので、 処理するものの性質により変更することができます。たとえば、 遅延リードのような処理はUIの裏側でこっそりとやってほしいので、 THREAD_PRIORITY_IDLE でいいと思います。しかし、ファイルから検索を行い、中止のUIを表のスレッドで行う時のような場合は、 通常のプライオリティがよいでしょう。
コードは以下のような感じになります。
static UINT doBeginDRDThread( LPVOID pParam ) // スレッド制御関数 { CViviDoc *pDoc = (CViviDoc *)pParam; ..... // pDoc に対する処理 return 0; } void CViviDoc::foo() { ..... AfxBeginThread(doBeginDRDThread, // ワーカスレッドを起動する (LPVOID)this, // このオブジェクトへのポインタを渡す THREAD_PRIORITY_IDLE); // 暇な時にこっそりやってね ..... }
この例では、スレッド制御関数にクラスのオブジェクトのポインタを渡しています。 これは筆者のお勧めで、オブジェクトに対し、マルチスレッド処理を行う場合のひとつの方法です。なお、 制御関数の中でクラスの非パブリックなメンバにアクセスしたい場合は関数をクラスのフレンドにします。 または、うら仕事を行うパブリックなメソッドを定義し、スレッド制御関数からはそのメソッドを呼ぶ、 というのもいわゆるひとつの方法です。
通常ワーカスレッドは時間のかかる処理をバックグラウンドで行わせるわけですから、
処理が終了するまである程度の時間を要するのが普通です。したがって、まっとうなUIであれば、
処理を中断できる機能が必須になります。
処理を中断する方法はいくつかあると思いますが、グローバル変数を使用するのが最もお手軽です。
その変数を中断フラグと呼ぶことにします。ワーカスレッドを起動する前に中断フラグをOFFにし、
ワーカスレッドでは処理のひと区切りごとに中断フラグを参照し、フラグがONの場合はスレッドを終了させます。
UIの方ではユーザが処理の中断を指示したら中断フラグをONにし、起動したスレッドが終了するのを待ちます。
中断フラグは複数のスレッドから同時に参照されますが、更新を行うのはUIを行うスレッドだけなので、
同期をとる必要はありません。
│ ↓ ┏━━━━━━━━━━━━━━┓ ┃AfxBeginThread┠────┐ ┗━━━━━━┯━━━━━━━┛ │ スレッドを起動 │ ↓ │ 処理中断 ┏━━━━━━━┓ │───────→┃ワーカスレッド┃ │ ┗━━━━━━━┛ │ │ │←───────────┘ 処理が終了、または中断 ↓
上図はワーカスレッドの起動と中断を模式的に表したものです。 この図ではスレッドを起動する側の処理がシーケンシャルなように記述していますが、 実際にはUIによるイベントドリブンなものになります。
複数のスレッドで同一オブジェクトにアクセスする場合、思いもよらない結果になることがあります。
例えば、単純な int 型の変数に対しても、
2つ以上のスレッドが参照と更新を行うと結果が保証されないのは明らかです。また、
ワーカスレッドの中断で説明した中断フラグの様にひとつのスレッドだけが更新を行う場合、
同期をとる必要はありませんが、参照・更新するオブジェクトが CStringList のような複雑なものの場合、
更新処理の途中でオブジェクトにアクセスされると、不正な状態を参照することになります。
このような問題に対処するには、同期処理(Synchronization)を行います。 MFCには同期をとるための同期クラスがいくつかあります。ここでは、もっとも簡単に使用できる CCriticalSection について簡単に説明します。同期クラスには、この他にも,CMutex, CSemaphore という同期オブジェクト(後の方ほど機能が強力)と CSingleLock、CMultiLock という同期アクセスオブジェクトがあります。これらは高度な機能を提供しており、 より複雑な場面で利用します。それらの詳細はヘルプを参照してください。
CCriticalSection を使うと、特定の区間のコードの実行が同一プロセスの他のスレッドにより中断されない
ことを保証出来ます。すなわち、
プロセス内の複数のスレッドでひとつのオブジェクトを同期的に参照・更新したい時に使用できます。
手順としては、
となります。
コードは以下の様な感じです。
class CMySyncClass { // 同期をとる CCriticalSection m_scriticalSection; ..... public: ..... BOOL Lock() { return m_scriticalSection.Lock(); }; BOOL Unlock() { return m_scriticalSection.Unlock(); }; }; CMySyncClass m_syncObject; // 同期オブジェクトの生成 m_syncObject.Lock(); // クリティカルセクションに入る ..... // m_syncObject の参照・更新 m_syncObject.Unlock(); // クリティカルセクション終了
前のTIPS 次のTIPS 津田伸秀 のホームページに戻る。
Last Updated on 30-Aug-1996, Copyright (c) 1996 by Nobuhide Tsuda, All Right Reserved.
このホームページに関するご質問、ご要望、バグレポート等は
Nobuhide_Tsuda@jsn.justnet.or.jp
までメールをいただければ幸いです。