◆◆ スレッド ◆◆
SFML2.3 非公式日本語翻訳
スレッドって何?
みんなのアイドル、スレッドちゃん。
え、スレッドちゃんを知らない?
そんなキミのために、まずはスレッドとは何なのかを、カンタンに説明するよ。
スレッドとは……「命令の列」です。
「命令の列」は、他の「命令の列」と平行して実行することができます。
どんなプログラムでも、最低1つのスレッドで出来てます。
いわゆる「メインスレッド」ですね。C言語で言うと main() で実行されるアレです。
メインスレッドだけのプログラムは「シングルスレッド」と呼ばれます。
そこへ、別のスレッドを追加すると「マルチスレッド」ということになります。
つまりスレッドを使うと、同時に複数の命令を実行できるようになります。これは便利!
たとえば、キャラのアニメーションの表示と、ユーザーからの入力受付と、画像や音声のローディングを同時にできます。
ネットワークプログラミングの世界でもスレッドは大活躍です。
画面の表示処理を続けながら、ネットワークからのデータを待ち受けることができます。
SFMLのスレッド機能 vs C++標準のスレッド機能
C++の新しいバージョン(2011)では、標準ライブラリにスレッドの機能が含まれてます。
SFMLが制作された当時の C++ にはスレッドの機能がなく、スレッドを扱うための「標準的な方法」がありませんでした。
SFML2.0がリリースされたときには、まだ、この機能に対応していない C++ コンパイラもたくさんありました。
もし、あなたが使ってるコンパイラが、C++標準のスレッド機能に対応しているなら、
SFMLのスレッド機能のことは忘れてくださいませ。あちらの方が高性能です。
(<thread>ヘッダー が使えるなら、対応しています)
SFMLのスレッド機能がグッドなソリューションになるのは、次のような場合です。
- 2011以前のコンパイラを使ってる場合
- ソースコードを配布して、いろいろなコンパイラで使えるようにしたい場合
SFML で スレッドを生成
前フリはこのぐらいにして、サンプルコードを見てみましょう。
SFML でスレッドを使うには、sf::Thread クラスを使います。
#include <SFML/System.hpp>
#include <iostream>
void func()
{
// thread.launch() が呼ばれたとき、この関数がスタートします。
for (int i = 0; i < 10; ++i) {
std::cout << "I'm thread number one" << std::endl;
}
}
int main()
{
// スレッドを生成。func() 関数を、スレッドのエントリーポイントとします。
sf::Thread thread(&func);
// スレッドを実行。
thread.launch();
// スレッドを実行している間も、メインスレッド(ここ)は止まらずに続く……
for (int i = 0; i < 10; ++i) {
std::cout << "I'm the main thread" << std::endl;
}
return 0;
}
thread.launch() が実行された後、main() と func() が平行して動きます。
結果として、両方の関数で生成されるテキストが、コンソール上に混ざって表示されます。
sf::Thread クラスのコンストラクタに、スレッドのエントリポイントにしたい関数を渡します。
sf::Thread クラスは、いろんな種類の関数をエントリーポイントとして受け取ることができます。
(非メンバー関数、メンバー関数、引数あり、引数なし、などなど)
上のサンプルでは、非メンバー関数を使ってます。
以下、その他のパターンのサンプルを示します。
【非メンバー関数。引数1つ】
void func(int x)
{
}
sf::Thread thread(&func, 5);
【メンバー関数】
class MyClass
{
public:
void func()
{
}
};
MyClass object;
sf::Thread thread(&MyClass::func, &object);
【関数オブジェクト(ファンクター)】
struct MyFunctor
{
void operator()()
{
}
};
sf::Thread thread(MyFunctor());
関数オブジェクトを使う機能は、最もパワフルです。
なぜなら、どんなタイプの関数オブジェクトでも使えるし、
そのおかげで サポートされてないタイプの関数でも、sf::Thread で使えるようになるからです。
特に C++11 のラムダや std::bind との組み合わせがアツイです。
// ラムダの例
sf::Thread thread([](){
std::cout << "I am in thread!" << std::endl;
});
// std::bind の例
void func(std::string, int, double)
{
}
sf::Thread thread(std::bind(&func, "hello", 24, 0.5));
sf::Thread をクラス内で使うときには、次のことを忘れないように。
sf::Threadクラスには、デフォルトコンストラクタがありません。
なので、(あなたの)クラスのコンストラクタの初期化子に書いておく必要があります。
class ClassWithThread
{
public:
// 初期化子つきのコンストラクタ
ClassWithThread() : m_thread(&ClassWithThread::f, this)
{
}
private:
void f()
{
...
}
// メンバー変数としてのスレッドクラス。
sf::Thread m_thread;
};
どうしても、(あなたの)クラスのコンストラクタを通過した後で sf::Thread のインスタンスを作りたい場合は、
new を使ってヒープ上に作ることもできます。
スレッド開始
sf::Thread のインスタンスを生成した後、launch() 関数でスレッドを実行です。
sf::Thread thread(&func);
thread.launch();
launch()関数を呼ぶと、スレッドのコンストラクタに渡した関数(エントリーポイント)が実行されます。
そして、処理がすぐに戻ってきます。つまり、処理を止めずに平行して実行を続けることができます。
スレッド停止
スレッドの関数(エントリーポイントとして std::Thread に渡した関数)が終了すると、スレッドは自動的に停止します。
スレッドの外から、そのスレッドが終了するのを待ちたい場合は、wait()関数を使います。
sf::Thread thread(&func);
// スレッド開始
thread.launch();
...
// 待機。スレッドが終了するまで、ここで処理がブロックされる。
thread.wait();
実は、wait()関数は、sf::Thread のデストラクタの中でも密かに呼ばれてます。
そのおかげで、sf::Thread が破棄された後に スレッドが残り続ける(メモリリーク)ことが防がれてます。
スレッドのライフサイクルの管理は重要なことなので、覚えておいてね!
(このページの最後の項「
よくあるミス」も参照のこと)
一時停止
sf::sleep() 関数で一時停止です。
ですが、「他のスレッドを」一時停止させる機能は sf::Thread にはありません。
一時停止は、そのスレッドが自分自身で行う必要があります。
つまり「現在のスレッド」だけを停止できる、とも言えますね。
void func()
{
...
sf::sleep(sf::milliseconds(10));
...
}
sf::sleep() 関数には引数が1つあります。停止する時間です。
数字の単位は任意です。
(「
時間を扱う」のチュートリアルを見てね)
ここで1つ特記事項!
sleep()関数では、どんなスレッドでも一時停止できます。つまり、メインスレッドも、です。
sf::sleep() 関数は「最も効率的な」一時停止の方法です。
この方法でスレッドを眠らせている間は、CPU使用率がゼロになります。
一方、while の空ループなんかを使った日には、CPU使用率が 100% になってしまいます。ただの待機に全力を尽くすぜ!
ちなみに、sleep() での一時停止の時間はあくまで目安と考えてください。OSによって、誤差が出ます。
数字通りの正確な待機時間になるとは期待しないでね!
同じプログラム上で動いているスレッドたちは同じメモリを共有しています。
どのスレッドも、プログラム内の変数にアクセスできます。便利ですが、危ない。
1つの変数や関数が、複数のスレッドから同時にアクセスされるかもしれない。これは危ない。
その機能がスレッドセーフ(マルチスレッドでの使用を想定している)ではない場合、
同時アクセスの結果がどうなるかはパルプンテです。(クラッシュしたり、あり得ないデータになったり)
「排他制御」を行うと、共有データを保護してスレッドセーフにすることができます。
排他制御の代表的なものとして、ミューテックス、セマフォ、コンディション、スピンロックなどがありますが、
考え方はどれも同じです。
「特定のコードを、特定のスレッドだけがアクセスできるようにして、その間、他のスレッドからのアクセスをブロックする」
中でも「ミューテックス」が一番ポピュラーです。
(ちなみにミューテックス(Mutex)は "MUTual(相互の) EXclusion(排他)" の略)
特定のコードを、同時に1つのスレッドからのみアクセスできるようにします。
では、サンプルを見てみましょう。
#include <SFML/System.hpp>
#include <iostream>
// ミューテックスオブジェクトを宣言
sf::Mutex mutex;
void func()
{
mutex.lock(); // ロック
for (int i = 0; i < 10; ++i) {
std::cout << "I'm thread number one" << std::endl;
}
mutex.unlock(); // ロック解除
}
int main()
{
sf::Thread thread(&func);
thread.launch();
mutex.lock(); // ロック
for (int i = 0; i < 10; ++i) {
std::cout << "I'm the main thread" << std::endl;
}
mutex.unlock(); // ロック解除
return 0;
}
上のサンプルでは、コンソールが「どのスレッドからもアクセスできる共有リソース」です(std::cout)。
以前のサンプルでは、コンソール上でテキストの表示が混ざる、という(望ましくない)結果になったのでした。
今回のサンプルでは、2つのスレッドの対応する処理を、
それぞれ、mutex.lock()~mutex.unlock() で囲んでいます。
これで、テキストが混ざらずに表示されるようになります。
順を追って、処理を見ていきます。
先に mutex.lock() に到達したスレッドが、ミューテックスのロックに成功します。
そして、その後のコードにアクセスして、テキストを出力します。
もう片方のスレッドの処理が mutex.lock() に到達した時点で既にミューテックスがロックされているとき、
そのスレッドはスリープ状態になります。ちなみにこの間、CPU使用率はゼロです(sf::sleep と同じ)。
最初のスレッドがミューテックスのロックを解除(mutex.unlock())すると、
第2のスレッドがスリープから目覚めて、ミューテックスのロックを行い、続く処理を実行します。
と、このように、コンソール上にテキストが混ざることなく順番に表示されます。
ミューテックスの他にも排他制御の方法はあります。
ですが、ほとんどの場合はミューテックスで充分です。
もし、すごくフクザツなスレッド処理をしていて、ミューテックスだけでは物足りない、という場合は、
遠慮しないで SFML 以外のライブラリに浮気してください。
ミューテックスを保護
心配無用! ミューテックスはすでにスレッドセーフです。
わざわざ保護してあげなくても大丈夫です。
でも、ミューテックスは「例外セーフ」ではないのでした!
では、ミューテックスがロックされている間に例外が発生すると、一体どうなってしまうのか?
ロックが解除されなくなってしまいます。永遠にロックされたまま。
そして、ミューテックスをロックしようとした他の全てのスレッドは、ブロックされたままになります。
つまり、アプリケーションがフリーズします。これは大変!
例外が発生するかもしれない環境でミューテックスが必ずロック解除されるようにするために、
SFML は RAII クラスを用意しています(sf::Lock)。
これで ミューテックスをラッピングすると、このクラスのコンストラクタが内部でミューテックスをロックしてくれます。
そして、デストラクタ内でロックを解除してくれます。単純で効果的。
sf::Mutex mutex;
void func()
{
sf::Lock lock(mutex); // mutex.lock() が実行される。
// 例外が発生するかもしれない関数
functionThatMightThrowAnException(); // 例外が発生したら mutex.unlock() が実行される
} // mutex.unlock()
// 関数の終了とともに lockインスタンスが解放され、
// そのデストラクタ内で mutex.unlock() が実行される。
sf::Lock は 複数の return がある関数の中でも便利です。
sf::Mutex mutex;
bool func()
{
sf::Lock lock(mutex); // mutex.lock()
if (!image1.loadFromFile("...")) {
return false; // mutex.unlock();
}
if (!image2.loadFromFile("...")) {
return false; // mutex.unlock()
}
if (!image3.loadFromFile("...")) {
return false; // mutex.unlock()
}
return true;
} // mutex.unlock()
※訳註:
RAII(Resource Acquisition Is Initialization)については、
Wikipedia の解説がわかりやすいと思います。
よくあるミス
sf::Thread のインスタンスがないと、スレッドは存在できません。
なので、下記のようなコードは失敗します。
void startThread()
{
sf::Thread thread(&funcToRunInThread);
thread.launch();
}
int main()
{
startThread();
// ...
return 0;
}
startThread() 内でスレッドを起動して、スレッドが自立して動き続けて、スレッドが終わるときに破棄される……
と、いいのですが、そうはなってくれません。
立ち上げたスレッドが、メインスレッドをブロックしているような動作になります。
まるでマルチスレッドではないかのように……。
なぜそうなるのか?
sf::Threadのインスタンスが startThread() 関数内のローカル変数だからです。
つまり、 startThread() 関数が終了するときに破棄されてしまいます。
sf::Thread インスタンスのデストラクタが起動され、内部で wait() を実行します。
その結果、メインスレッドがブロックされ、スレッドが終了するのを待ち続けることになります。並列処理にはならない。
(wait()関数については、「
スレッド停止」の項も参照してね)
そんなわけで、スレッドが実行されている間は、対応する sf::Thread のインスタンスも存在している必要があります。
そうなるように sf::Thread のインスタンスのライフサイクルを管理してくださいませ。