今回はマルチスレッドについてです。
スレッドとは、非同期に動く関数のようなもので、
イメージとしては、上司をスレッドを作る側、部下をスレッドとした場合に、
上司が「この仕事やっといて」と部下に頼む(スレッドを作る)と、上司が別の仕事をやっていても、
部下が頑張って仕事を仕上げてくれるといった感じです。
御存知の通り、BeOSはメッセージループなどが、高度にマルチスレッド化されていて
自分でわざわざスレッドを作る必要は無いように感じるかもしれません。
しかし、ネットワークプログラムはデータの送受信の時などに、プログラムがストップしてしまう可能性があります。
コマンドラインツールでは問題がないのですが、BeOSの様なGUIのアプリケーションでは、
再描画が止まってしまうために、ユーザーからは一見応答が無いように見えてしまうという欠点があります。
また、スレッドを作ると処理を単純化出来る局面もあります。
さらに、マルチCPU環境の場合、複数のCPUを有効に活用することも出来ます。
(実際には単一のスレッドに重い処理が集中して、単一のアプリケーションで有効活用するには難しいんですけどね)
今回のサンプルはコンソールアプリケーションです。
今までのBApplicationをベースとしたプログラムでは、スレッドの概念になじみの無い方に対して、
理解が難しいと想ったので、こうしました。
したがって、今までのプログラムとは見た目がガラッと変わりますが、特に難しい処理はしていないので、
安心して見てください。
// sample15.cpp
#include <OS.h> // スレッド生成と時間情報のために必要
#include <iostream.h> // カウンター表示のため(cout)に必要
// スレッドによるカウンタークラス
class ThreadAutoCounter
{
public:
ThreadAutoCounter(uint64 count);
status_t StartCounter(); // カウンターのスタート
status_t StopCounter(); // カウンターのストップ
void PrintCount(); // カウンター内容を標準出力に表示
private:
static int32 CounterFunc(void* data); // スレッド用関数
thread_id m_tid; // スレッドID
uint64 m_count; // カウンターの値
};
// コンストラクタは、カウンターの初期値を与える
ThreadAutoCounter::ThreadAutoCounter(uint64 count)
:m_count(count)
{
}
// カウンターをスタートさせる関数
status_t ThreadAutoCounter::StartCounter()
{
m_tid = spawn_thread(ThreadAutoCounter::CounterFunc,
"CounterThread",B_LOW_PRIORITY,
this);
return resume_thread(m_tid);
}
status_t ThreadAutoCounter::StopCounter()
{
return kill_thread(m_tid);
}
// カウンターを表示する関数
void ThreadAutoCounter::PrintCount()
{
cout << "Counter:" << m_count << endl;
}
// カウンターを増やすスレッド用関数
int32 ThreadAutoCounter::CounterFunc(void* data)
{
// 自分自身のクラスへキャスト
ThreadAutoCounter* counter;
counter = reinterpret_cast<ThreadAutoCounter*>(data);
// 1000までカウント
for (int i=0;i<1000;i++)
{
snooze(30000); // 3ミリ秒待つ
counter->m_count++; // カウンターを1足す
}
return B_OK; // スレッドの正常終了時にはB_OKを返す
}
int main()
{
ThreadAutoCounter counter(0); // カウンターの初期値は0
counter.StartCounter(); // カウンターをスタート
bigtime_t start_time;
start_time = system_time(); // OSの起動時間をマイクロ秒で取得
// 10秒間(10000000マイクロ秒)カウンターの内容を表示し続ける
while (system_time() - start_time < 10000000)
{
snooze(30000); // 3ミリ秒待つ
counter.PrintCount(); // カウンターの内容を表示
}
counter.StopCounter(); // カウンタースレッドを破棄
return 0;
}
MuTerminalで実行した実行結果です。
カウンタの内容がインクリメントされるのが表示されるだけの、あまり面白くないものです。
本当にサンプルのためのプログラムって感じですかね。
BeOS上でスレッドを利用するには、Cの関数でも可能なのですが、
やはりクラスに組み込んで使用することが多いと思います。
そこで、まずスレッドを使用したカウンターのクラス宣言の部分を見てみましょう。
// スレッドによるカウンタークラス
class ThreadAutoCounter
{
public:
ThreadAutoCounter(uint64 count);
status_t StartCounter(); // カウンターのスタート
status_t StopCounter(); // カウンターのストップ
void PrintCount(); // カウンター内容を標準出力に表示
private:
static int32 CounterFunc(void* data); // スレッド用関数
thread_id m_tid; // スレッドID
uint64 m_count; // カウンターの値
};
ここで、注目すべきは、thread_idという型が登場していることと、スレッド用関数(CounterFunc)の宣言の頭にstaticがついていることです。
BeOS上では、スレッドすべてにID番号が割り振られていて、スレッドの生成や開始、
強制終了などは、このID番号を介して行います。
そのための整数の型がthread_idなのです。
これは、OS.h内で、int32の型のtypedefになっています。
次にスレッド用関数の宣言の頭にstaticがなぜついているのか、ということの説明です。
クラスのメンバ関数の頭にstaticをつけるというのは、見慣れない人にはどういう意味があるのかわからないかもしれませんが、
これは、クラスのインスタンスを作らなくても(newしなくても)、アクセス出来る関数ということです。
通常、クラスのメンバ関数というものは、オブジェクトを構築した後ではないと、呼び出すことが出来ませんが、
staticメンバ関数では、この場合ThreadAutoCounter::CounterFuncと行った感じに、
スコープ解決演算子( :: )で、クラス名と関数名を繋ぐことにより、メンバ関数にもアクセスすることが出来ます。
まず、カウンタークラスのコンストラクタを見てみましょう。
// コンストラクタは、カウンターの初期値を与える
ThreadAutoCounter::ThreadAutoCounter(uint64 count)
:m_count(count)
{
}
コンストラクタでは、カウンターの値m_countに対して、引数の値で初期化しているだけです。
クラスのコンストラクタでは、コロン( : )の後に、「メンバ変数名(値)」と書くことにより、
メンバ変数の値を初期化することが、出来ます。
C++では、このコロンのことを初期化指定子なんて言い方をしたりします。
次に、カウンターをスタートさせるメンバ関数、StartCounter()を見てみましょう。
// カウンターをスタートさせる関数
status_t ThreadAutoCounter::StartCounter()
{
m_tid = spawn_thread(ThreadAutoCounter::CounterFunc,
"CounterThread",B_LOW_PRIORITY,
this);
return resume_thread(m_tid);
}
ここで、出てきている関数は
の2つです。
まず、spawn_threadの宣言を見てみましょう。
thread_id spawn_thread ( thread_func function_name, const char *thread_name, int32 priority, void *arg );
引数には、
を指定します。
まず、thread_funcに非同期に走らせるスレッド関数のポインタを指定しますが、
thread_funcは次のようにtypedefされています。
typedef int32 (*thread_func) (void *);
これまた、見慣れない人には驚異かもしれませんが、これは
引数にvoid*の値を1つとり、int32を返す関数へのポインタ
という意味です。
また、スレッドのポインタを渡すといっても、あまり難しく考える必要はなく、
スレッド関数の名前をそのまま書けばいいだけです。
この場合は、staticなメンバ関数がスレッド関数ですので、スコープ解決演算子( :: )を使用して、クラス名とメンバ関数名を繋げたものを指定しています。
スレッドの名前である、thread_nameはなんのために存在するかというと、
スレッドを名前で検索する、find_thread関数で使います。
これは、スレッドの名前を引数にとり、見つかったらthread_idを返す関数です。
thread_id find_thread(const char *name);
ただし、スレッドの名前がぶつかった時の考慮は全然されてなく、
最初に見つかったthread_idを返すだけの関数ですので、注意が必要です。
また、スレッドの名前はB_OS_NAME_LENGTHの長さまでしか指定できません。
R5.0時点では、B_OS_NAME_LENGTHは32のマクロですので、スレッドの名前はヌル文字を含め32文字までしか指定できません。
次に、スレッドの優先度指定、priorityです。
これは、数が大きいほどスレッドの優先度が高い(よく実行される)という意味ですが、
自分で好き勝手な値をしていするよりは、OS.h内に書いてある次の定数を指定することが推奨されています。
| 値 | 実際の数字 |
|---|---|
| B_LOW_PRIORITY | 5 |
| B_NORMAL_PRIORITY | 10 |
| B_DISPLAY_PRIORITY | 15 |
| B_URGENT_DISPLAY_PRIORITY | 20 |
| B_REAL_TIME_DISPLAY_PRIORITY | 100 |
| B_URGENT_PRIORITY | 110 |
| B_REAL_TIME_PRIORITY | 120 |
普通は、B_NORMAL_PRIORITYを指定するだけでいいでしょう。
この場合は、カウンタを足すという単純な処理なので、もっとも最低のB_LOW_PRIORITYを指定しています。
最後に、スレッドに渡す値です。
先程も行ったように、staticなメンバ関数は、クラスのインスタンスを生成しなくてもアクセス出来る関数のことですが、
しかし、インスタンスを生成しなくてもいいということは、そのままではインスタンス特有の情報にアクセス出来ないという意味でもあります。
この場合でいうと、スレッドIDのm_tidや、カウンタの値のm_countとかにアクセス出来ません。
それを補うために、自分自身のインスタンスをスレッド関数に渡して(thisを指定して)、
カウンタの値にCounterFuncがアクセス出来るようにします。
さて、以上の情報を与えたら、thread_idが返ってきてスレッドが生成されたことになります。
仮にエラーが発生した場合、マイナスの整数が返ってきます。
つまり正常時にはthread_idはすべてプラスの整数なのです。
spawn_threadでthread_idが返ってきても、スレッドはまだ動いてはいません。
いわば眠っている状態です。
これを、スレッドの一時停止状態といいます。
スレッドを開始させるためには、resume_threadを使います。
status_t resume_thread(thread_id thread);
resume_threadの戻り値の意味は次の通りです。
| 値 | 意味 |
|---|---|
| B_NO_ERROR | 問題なし |
| B_BAD_THREAD_ID | 渡されたのは有効なスレッドIDではない |
| B_BAD_THREAD_STATE | スレッドは一時停止状態ではない |
このメンバ関数では、resume_threadの戻り値を返していますが、特に厳密なエラーチェックは行っていません。
resume_threadはスレッド関数に1回だけ「動け」と命令して、自分自身はすぐに返ってきます。
スレッド関数は、OS上のどこかで一生懸命働いていますが、スレッドを生成した方はそんなことはお構いなしです。
次に、スレッド関数である、CounterFunc内を見てみましょう。
// カウンターを増やすスレッド用関数
int32 ThreadAutoCounter::CounterFunc(void* data)
{
// 自分自身のクラスへキャスト
ThreadAutoCounter* counter;
counter = reinterpret_cast<ThreadAutoCounter*>(data);
// 1000までカウント
for (int i=0;i<1000;i++)
{
snooze(30000); // 3ミリ秒待つ
counter->m_count++; // カウンターを1足す
}
return B_OK; // スレッドの正常終了時にはB_OKを返す
}
何度も言いますが、スレッド関数はstaticであるために、インスタンス特有の情報にアクセス出来ません。
そこで、引数として渡された値(data)を、自分自身のクラスへキャストするという作業が必要になります。
この渡された値というのは、spawn_threadの第4引数の値です。
次に、カウンタを1ずつ足すforループに入っていますが、
ここでループ一回ごとに、ちょっとずつ待つために、snooze関数を使用しています。
snooze関数は呼び出し元のスレッドを何マイクロ秒間か寝かせる関数です。
status_t snooze(bigtime_t microseconds);
引数に、寝かせる時間をマイクロ秒単位で指定します。
戻り値は次の通りです。
| 値 | 説明 |
|---|---|
| B_OK | 問題なし |
| B_INTERRUPTED | 寝ている間にシグナルを受け取った |
snoozeは「待つ」のではなく、「寝る」ということに注意してください。
スレッドがsnoozeの間は、CPUは暇になり別のスレッドの実行に使えるため、結果的にCPU使用率を減らすことにもつながります。
次に、カウンターをストップさせるStopCounter()関数を見てみましょう。
これはkill_thread関数のサンプルにもなっています。
status_t ThreadAutoCounter::StopCounter()
{
return kill_thread(m_tid);
}
いやあ、いたって簡単ですね。
kill_threadは、killしたい(強制終了したい)スレッドのIDを指定します。
status_t kill_thread(thread_id thread)
戻り値は、例によって次の通りです。
| 値 | 説明 |
|---|---|
| B_OK | 大丈夫 |
| B_BAD_THREAD_ID | 有効なスレッドIDではない |
PrintCount関数は、カウンタの値を表示しているだけです。
// カウンターを表示する関数
void ThreadAutoCounter::PrintCount()
{
cout << "Counter:" << m_count << endl;
}
で、やっとmain関数の説明です。
main関数では、カウンターのスレッドを開始させて、あとはひたすらカウンターの内容を表示しているだけです。
int main()
{
ThreadAutoCounter counter(0); // カウンターの初期値は0
counter.StartCounter(); // カウンターをスタート
bigtime_t start_time;
start_time = system_time(); // OSの起動時間をマイクロ秒で取得
// 10秒間(10000000マイクロ秒)カウンターの内容を表示し続ける
while (system_time() - start_time < 10000000)
{
snooze(30000); // 3ミリ秒待つ
counter.PrintCount(); // カウンターの内容を表示
}
counter.StopCounter(); // カウンタースレッドを破棄
return 0;
}
main関数の中ではカウンターの表示「しか」していないという所に注目です。
カウンタースレッドがどこかで頑張っているおかげで、main関数内ではカウンターの表示に専念できるわけです。
main関数内で、初登場の関数にsystem_time()関数があります。
これは、BeOSが起動されてから、今までの時間をマイクロ秒単位で返します。
Windowsで言うところの、GetTickCount()やtimeGetTime()関数に当たるものです。
bigtime_t system_time (void);
bigtime_t型は、int64型であるとtypedefされており、64ビットの符号ありの整数型です。
システムカウンタは263/109/60/60/24/365 = 292.47となって、
292年連続稼働しないとあふれない計算になるので、安心してもいいでしょう(笑)。
まぁ、上の計算ではうるう年は考慮していませんけどね。
system_time()関数はゲームの描画タイミングや、CPUのクロック早さに依存せずに
同じ時間待ちたいときなどに、威力を発揮します。
ちょっと話題としてはそれますが、system_time()と似たような関数にreal_time_clock()関数があります。
これは、1970年1月1日からの値を秒単位で返しますが、戻り値はuint32型です。
また、逆に時刻を設定する関数の、set_real_time_clock()関数は、引数にint32型を取ります。
ここから逆算すると、set_real_time_clock関数では、231/60/60/24/365 = 68.10となってしまい、
1970 + 68 = 2038年に限界が来てしまう計算になります。
real_time_clock()関数の戻り値がuint32なのは、桁あふれを起こさないための苦肉の策と思われます。
符号ビットを取り除くだけで、単純に68年を2倍して136年持つ計算になりますから。
スレッドの生成について長々と書いてきましたがいかがでしたでしょうか?
自分の経験で言うと、関数のポインタが出てきただけでスレッドを怖がっていたのですが、
いったん親しんでしまえば、全然難しくありません。
しかし、これだけではスレッド間の同期の問題が残っています。
このサンプルのように、作成するスレッドが1つだけだったり、
スレッド間で共通の資源(変数)を共有しない場合はいいのですが、
実際には、そのようにはならないことがほとんどです。
そのあたりについて、次回は解説したいと思います。
|
|||
|
|