メッセージ分割
<< 「マルチスレッド開発の傾向と対策」に戻る
概要
これは、あるスレッドから別のスレッドにメッセージをポストして、仕事の依頼を行うという考え方である。
その為、少なくとも受信側のスレッドはメッセージループを使用している必要がある。一般的には、ウインドウシステムが持つメッセージ機構をそのまま流用する。
MFCであれば、CWinThreadクラスを使う。
理論
要求を受け付けるスレッドは、常にメッセージループで待ち合わせを行っている。そして、要求を発行する側のスレッドは、相手のスレッド(が待ち合わせを行っているメッセージキュー)に、メッセージをポストする。

メッセージキューにメッセージがポストされると、要求を受け付ける側のスレッドはそのメッセージを取り出し、要求された処理、つまりイベントハンドラを実行することになる。
この形式を採用する場合、要求を受け付けるスレッドは、メッセージキューとイベントハンドラを合わせて1つのクラスとして、そのクラスのインスタンス一つに対して、スレッドが1つく括りつくようにするのが良いようだ。
なぜかというと、そうすることにより、既存のウインドウシステムの機構や、MFC等のクラスライブラリが利用しやすくなるからだ。
そしてこの考えを推し進めておくと、原理的な意味でのオブジェクト指向の概念に近い世界ができあがる事になる。

各インスタンスはそれぞれ独自のスレッドを持ち、完全に非同期に活動する。そして、時々他のインスタンスに対してメッセージを送出する。
送出されたメッセージは相手のインスタンスの都合に従って非同期に処理される。
まぁ、俺にはこんなあやふやな、非同期だらけの設計は、到底実用的とは思われないが、それでも実験的な意味では面白いかも知れない。
実装
MFCが利用できるのなら、AppWizard等もあり簡単に上記の方式を実装することが可能となるだろう。
だが、その方法だと環境や方式にかなり強い制約が課せられることになるので、ここはあえて全てを自分で作成する道を選びたいと思う。
#include <stdio.h>
#include "Thread.h"
#include "Semaphore.h"
#include <list>
using namespace std;
// メッセージキュー
class CMessageQueue
{
public:
CMessageQueue() :
semaQueue(),
semaWait()
{
// 初期状態では、メッセージキューは空である
semaWait.P();
};
// キューにメッセージを追加する
void push( int r )
{
semaQueue.P(); // キューの排他制御
theQueue.push_back( r ); // メッセージを追加
semaWait.V(); // semaWaitも加算する
semaQueue.V(); // 排他制御の解除
};
// キューからメッセージを取り出す
int pop()
{
int r;
semaWait.P(); // メッセージを待ち合わせる
semaQueue.P(); // キューの排他制御
// キューからメッセージを取り出す
r = *( theQueue.begin() );
theQueue.erase( theQueue.begin() );
semaQueue.V(); // 排他制御の解除
return r;
};
protected:
// メッセージキュー
list< int > theQueue;
// theQueueの排他制御用
CSemaphore semaQueue;
// メッセージ到着待ち
CSemaphore semaWait;
};
// メッセージキュー付きのスレッドクラス
class CThread2 : public CThread
{
public:
CThread2(){};
// スレッドのエントリポイント
void run()
{
int r;
// メッセージを取り出す
r = Queue.pop();
while ( r != 0 ) {
// メッセージに応じた処理を行う
printf( "this = 0x%08X : r = %d\n", this, r );
// メッセージを取り出す
r = Queue.pop();
}
};
// このスレッドにメッセージを送信する
void SendMessage( int r )
{
// メッセージをキューに追加する
Queue.push( r );
};
protected:
CMessageQueue Queue; // メッセージキュー
};
// 動作確認
int main()
{
CThread2 t1, t2;
t1.start();
t2.start();
t1.SendMessage( 1 );
t1.SendMessage( 2 );
t1.SendMessage( 3 );
t1.SendMessage( 4 );
t2.SendMessage( 9 );
t2.SendMessage( 8 );
t2.SendMessage( 7 );
t2.SendMessage( 6 );
t1.SendMessage( 0 );
t2.SendMessage( 0 );
Sleep( 1000 );
return 0;
}
|
メッセージキューの実装は、以前書いたこのページにある「生産者と消費者の問題」のものをほとんどそのまま使用している。
CMessageQueueクラスは、STLのlistを使って、int型の値をスレッドセーフにリストの末尾に追加する機能と、同時にスレッドセーフにリストの先頭からint型の値を取り出す機能を実装している。
CThread2クラスは、メンバとしてCMessageQueueクラスのインスタンスを持ち、自分のメッセージキューに送信されてきたメッセージ(単なるint型の値)をひたすら取得して、printfで画面に表示している。
main関数では、t1とt2の2つのスレッドを生成し、それぞれ1・2・3・4という値と、9・8・7・6という値をメッセージとして渡し、処理を行わせている。そして最後に、0を渡すことで処理の終了を通知している。
上記の処理を絵で表せば、下記のようになる。

上記の例では、イベントハンドラはメッセージの値によらずprintfを行っているだけであるが、実際にはメッセージの値に応じた処理を行うイベントハンドラを呼び出すことになる。
また、メッセージの内容も単純なint型だけでは、イベントハンドラに対して引数が渡せないため、もう少し複雑な構造体などを使用することになるだろう。
利用例
このパターンは、どんなことに利用すればいいのか。どのような場合に、このパターンを使って逃げればいいのか。
簡単に言ってしまえば、複数箇所から入ってくる入力データを監視し、それぞれの入力をリアルタイムに処理しなければならないような場合だ。それも、かなりリアルタイム性が求められる場合に限られる。
例えば、音声・映像・ユーザインターフェース・ネットワークの4つの入力それぞれに対して、然るべき処理を行い応答を返さなければならないとしたら、どうだろうか。

考えてみて欲しい。上記が、画像付きのチャットソフトのようなものだとして、音声や画像をリアルタイムに送受信できないのみならず、自分の声を録音している間は画像を録画することができず、当然その間は画面を操作することもできないとしたら、どう思われるだろうか。そんなソフトを使いたいと思うだろうか。
普通は、相手の姿を見ながら相手の声を聞き、同時に自分の声と姿を送信したいとは思わないだろうか。当然、必要に応じていつ何時でも画面の操作ができるようになっていなければならない。それが当たり前だろう。
だが、その当たり前を実現するのは容易ではない。何せ、複数のことを同時に行っているのだ。
カメラから画像を入力しつつ、マイクからは音声を入力しつつ、ネットワークからは相手側の音と映像が流れ込んでくる。ユーザはいつ画面を操作するか判らないから、常にウインドウシステムのメッセージキューを監視し続けなければならない。
これらをシングルスレッドで実現できるだろうか。工夫すればやってできないことはないのだろうが、とても大変だ。素直に考えれば、それぞれの処理を専門に行うスレッドを生成した方が、考えやすい。

では、それならばどうやって各スレッドを協調動作させるのか。スレッド間の通信をどうやって実現するのか。
そんなときに利用可能なのが、このパターンだ。
あるスレッドで何らかのイベントが発生し、別のスレッドに処理を依頼する必要が出てきたら、そのスレッドに対してメッセージを投げてやればいい。そうすれば、いつかはそのイベントに気がついて、必要な処理を行ってくれるに違いない。
だがもし、入力が複数あったとしても、それ程リアルタイム性が求められないのであれば、マルチスレッドにする必要はないし、するべきではない。ある程度の入力を蓄積しておき、シングルスレッドでまとめて処理する。その方が簡単だし、処理も効率的である。
<< 「マルチスレッド開発の傾向と対策」に戻る
|