この章では、マルチスレッドプログラムの構築方法を説明します。さらに、例外の使用、C++ 標準ライブラリのオブジェクトをスレッド間で共有する方法、従来の (旧形式の) iostream をマルチスレッド環境で使用する方法についても取り上げます。
マルチスレッド処理の詳細については、『マルチスレッドのプログラミング』、『Tools.h++ ユーザーズガイド』、『標準 C++ ライブラリ・ユーザーズガイド』を参照してください。
C++ コンパイラに付属しているライブラリは、すべてマルチスレッドで使用しても安全です。マルチスレッドアプリケーションを作成したい場合や、アプリケーションをマルチスレッド化されたライブラリにリンクしたい場合は、-mt オプションを付けてプログラムのコンパイルとリンクを行う必要があります。このオプションを付けると、-D_REENTRANT がプリプロセッサに渡され、-lthread が ld に正しい順番で渡されます。互換性モード (-compat[=4]) の場合、-mt オプションは libthread を libC の前にリンクします。標準モード (デフォルトモード) の場合、-mt オプションは libthread を libCrun の前にリンクします。
-lthread を使用してアプリケーションを直接リンクしないでください。libthread が誤った順番でリンクされます。
マルチスレッドアプリケーションのコンパイルとリンクを別々に行う場合は、次のように入力します。
example% CC -c -mt myprog.cc example% CC -mt myprog.o |
次のように入力すると、マルチスレッドアプリケーションが正しく構築されません。
example% CC -c -mt myprog.o example% CC myprog.o -lthread <- libthread が正しい順番でリンクされない |
ldd コマンドを使用すると、アプリケーションが libthread にリンクされたかどうかを確認できます。
example% CC -mt myprog.cc example% ldd a.out libm.so.1 => /usr/lib/libm.so.1 libCrun.so.1 => /usr/lib/libCrun.so.1 libthread.so.1 => /usr/lib/libthread.so.1 libc.so.1 => /usr/lib/libc.so.1 libdl.so.1 => /usr/lib/libdl.so.1 |
C++ サポートライブラリ (libCrun、libiostream、libCstd、libC) は、マルチスレッドで使用しても安全ですが、非同期安全 (非同期例外で使用しても安全) ではありません。したがって、マルチスレッドアプリケーションのシグナルハンドラでは、これらのライブラリに含まれている関数を使用しないでください。使用するとデッドロックが発生する可能性があります。
マルチスレッドアプリケーションのシグナルハンドラでは、次のものは安全に使用できません。
iostream
new 式と delete 式
例外
現在実装されている例外処理は、マルチスレッドで使用しても安全です。すなわち、あるスレッドの例外によって、別のスレッドの例外が阻害されることはありません。ただし、例外を使用して、スレッド間で情報を受け渡すことはできません。 すなわち、あるスレッドから送出された例外を、別のスレッドで捕獲することはできません。
それぞれのスレッドでは、独自の terminate() 関数と unexpected() 関数を設定できます。あるスレッドで呼び出した set_terminate() 関数や set_unexpected() 関数は、そのスレッドの例外だけに影響します。デフォルトの terminate() 関数の内容は、すべてのスレッドで abort() になります。「8.2 実行時エラーの指定」を参照してください。
-noex または -features=no%except が指定されている場合を除き、pthread_cancel(3T) の呼び出しでスレッドを取り消すと、スタック上の自動オブジェクト (静的ではない局所オブジェクト) が破棄されます。
pthread_cancel(3T) では、例外と同じ仕組みが使用されます。スレッドが取り消されると、局所デストラクタの実行中に、ユーザーが pthread_cleanup_push() を使用して登録したクリーンアップルーチンが実行されます。クリーンアップルーチンの登録後に呼び出した関数の局所オブジェクトは、そのクリーンアップルーチンが実行される前に破棄されます。
C++ 標準ライブラリ (libCstd -library=Cstd) は、いくつかのロケールを除けばマルチスレッドで使用しても安全なライブラリで、このライブラリの内部は、マルチスレッド環境で正しく機能することが保証されています。ただし、依然、スレッド間で共有するライブラリオブジェクト周りには注意を払う必要があります。setlocale(3C) および attributes(5) のマニュアルページを参照してください。
たとえば、文字列をインスタンス化し、この文字列を新しく生成したスレッドに参照で渡した場合を考えてみましょう。この文字列への書き込みアクセスはロックする必要があります。なぜなら、同じ文字列オブジェクトを、プログラムが複数のスレッドで明示的に共有しているからです。この処理を行うために用意されたライブラリの機能については後述します。
これに対して、この文字列を新しいスレッドに値で渡した場合は、ロックについて考慮する必要はありません。このことは、Rogue Wave の「書き込み時コピー」機能により、2 つのスレッドの別々の文字列が同じ表現を共有している場合にも当てはまります。このような場合のロックは、ライブラリが自動的に処理します。プログラム自身でロックを行う必要があるのは、スレッド間での参照渡しや、大域オブジェクトや静的オブジェクトを使用して、同じオブジェクトを複数のスレッドから明示的に使用できるようにした場合だけです。
ここからは、複数のスレッドが存在する場合の動作を保証するために、C++ 標準ライブラリの内部で使用されるロック (同期) 機能について説明します。
マルチスレッドでの安全性を実現する機能は、2 つの同期クラス、_RWSTDMutex と _RWSTDGuard によって提供されます。
_RWSTDMutex クラスは、プラットフォームに依存しないロック機能を提供します。このクラスには、次のメンバー関数があります。
void acquire()— 自分自身に対するロックを獲得する。または、このロックを獲得できるまでブロックする。
void release()— 自分自身に対するロックを解除する。
class _RWSTDMutex { public: _RWSTDMutex (); ~_RWSTDMutex (); void acquire (); void release (); }; |
_RWSTDGuard クラスは、_RWSTDMutex クラスのオブジェクトをカプセル化するための便利なラッパークラスです。_RWSTDGuard クラスのオブジェクトは、自分自身のコンストラクタの中で、カプセル化された相互排他ロック (mutex) を獲得しようとします。エラーが発生した場合は、このコンストラクタは std::exception から派生している ::thread_error 型の例外を送出します。獲得された相互排他ロックは、このオブジェクトのデストラクタの中で解除されます。このデストラクタは例外を送出しません。
class _RWSTDGuard { public: _RWSTDGuard (_RWSTDMutex&); ~_RWSTDGuard (); }; |
さらに、_RWSTD_MT_GUARD(mutex) マクロ (従来の _STDGUARD) を使用すると、マルチスレッドの構築時にだけ _RWSTDGuard クラスのオブジェクトを生成できます。生成されたオブジェクトは、そのオブジェクトが定義されたコードブロックの残りの部分が、複数のスレッドで同時に実行されないようにします。単一スレッドの構築時には、このマクロは空白の式に展開されます。
これらの機能は、次のように使用します。
#include <rw/stdmutex.h> // // 複数のスレッドで共有する整数 // int I; // // I の更新の同期をとるために使用する相互排他ロック (mutex) // _RWSTDMutex I_mutex; // // I を 1 だけ増分する。_RWSTDMutex を直接使用。 // void increment_I () { I_mutex.acquire(); // mutex をロック I++; I_mutex.release(); // mutex のロックを解除 } // // I を 1 だけ減分する。_RWSTDGuard を使用。 // void decrement_I () { _RWSTDGuard guard(I_mutex); // I_mutex のロックを獲得 --I; // // I のロックは guard のデストラクタが呼び出されたときに解除される // } |
この節では、libC ライブラリと libiostream ライブラリの iostream クラスを、マルチスレッド環境での入出力に使用する方法を説明します。さらに、iostream クラスの派生クラスを作成し、ライブラリの機能を拡張する例も紹介します。ここでは、C++ のマルチスレッドコードを記述するための指針は示しません。
この節では、従来の iostream (libC と libiostream) だけを取り扱います。この節の説明は、C++ 標準ライブラリに含まれている新しい iostream (libCstd) には当てはまりません。
iostream ライブラリのインタフェースは、マルチスレッド環境用のアプリケーション、すなわちサポートされている Solaris オペレーティングシステムのバージョンで実行される、マルチスレッド機能を使用するプログラムから使用できます。従来のライブラリのシングルスレッド機能を使用するアプリケーションは影響を受けません。
ライブラリが「マルチスレッドを使用しても安全」といえるのは、複数のスレッドが存在する環境で正しく機能する場合です。一般に、ここでの「正しく機能する」とは、公開関数がすべて再入可能なことを指します。iostream ライブラリには、複数のスレッドの間で共有されるオブジェクト (C++ クラスのインスタンス) の状態が、複数のスレッドから変更されるのを防ぐ機能があります。ただし、iostream オブジェクトがマルチスレッドで使用しても安全になるのは、そのオブジェクトの公開メンバー関数が実行されている間に限られます。
アプリケーションで libC ライブラリのマルチスレッドで使用しても安全なオブジェクトを使用しているからといって、そのアプリケーションが自動的にマルチスレッドで使用しても安全になるわけではありません。アプリケーションがマルチスレッドで使用しても安全になるのは、マルチスレッド環境で想定したとおりに実行される場合だけです。
マルチスレッドで使用しても安全な iostream ライブラリの構成は、従来の iostream ライブラリの構成と多少異なります。マルチスレッドで使用しても安全な iostream ライブラリのインタフェースは、iostream クラスやその基底クラスの公開および限定公開のメンバー関数を示していて、従来のライブラリと整合性が保たれていますが、クラス階層に違いがあります。詳細については、「11.4.2 iostream ライブラリのインタフェースの変更」を参照してください。
従来の中核クラスの名前が変更されています (先頭に unsafe_ という文字列が付きました)。iostream パッケージの中核クラスを表 11–1 に示します。
表 11–1 iostream の中核クラス
クラス |
内容の説明 |
---|---|
stream_MT |
マルチスレッドで使用しても安全なクラスの基底クラス |
streambuf |
バッファーの基底クラス |
unsafe_ios |
各種のストリームクラスに共通の状態変数 (エラー状態、書式状態など) を収容するクラス |
unsafe_istream |
streambuf から取り出した文字の並びを、書式付き/書式なし変換する機能を持つクラス |
unsafe_ostream |
streambuf に格納する文字の並びを、書式付き/書式なし変換する機能を持つクラス |
unsafe_iostream |
unsafe_istream クラスと unsafe_ostream クラスを組み合わせた入出力兼用のクラス |
マルチスレッドで使用しても安全なクラスは、すべて基底クラス stream_MT の派生クラスです。また、これらのクラスは、streambuf を除いて、(先頭に unsafe_ が付いた) 従来の基底クラスの派生クラスでもあります。この例を次に示します。
class streambuf: public stream_MT {...}; class ios: virtual public unsafe_ios, public stream_MT {...}; class istream: virtual public ios, public unsafe_istream {...}; |
stream_MT には、それぞれの iostream クラスをマルチスレッドで使用しても安全にするための相互排他 (mutex) ロック機能が含まれています。また、このクラスには、マルチスレッドで使用しても安全な属性を動的に変更できるように、ロックを動的に有効および無効にする機能もあります。入出力変換とバッファー管理の基本機能は、従来の unsafe_ クラスにまとめられています。したがって、ライブラリに新しく追加されたマルチスレッドで使用しても安全な機能は、その派生クラスだけで使用できます。マルチスレッドで使用しても安全なクラスには、従来の unsafe_ 基底クラスと同じ公開メンバー関数と限定公開メンバー関数が含まれています。これらのメンバー関数は、オブジェクトをロックし、unsafe_ 基底クラスの同名の関数を呼び出し、そのあとでオブジェクトのロックを解除するラッパーとして働きます。
streambuf クラスは、unsafe_ クラスの派生クラスではありません。streambuf クラスの公開メンバー関数と限定公開メンバー関数は、ロックを行うことで再入可能になります。ロックを行わない関数も用意されています。これらの関数は、名前の後ろに _unlocked という文字列が付きます。
iostream のインタフェースには、マルチスレッドで使用しても安全な、再入可能な公開関数が追加されています。これらの関数は、追加引数としてユーザーが指定したバッファーを受け取ります。これらの関数を次に示します。
表 11–2 マルチスレッドで使用しても安全な、再入可能な公開関数
関数 |
内容の説明 |
---|---|
char *oct_r (char *buf, int buflen, long num, int width) |
数値を 8 進数の形式で表現した ASCII 文字列のポインタを返す。width が 0 (ゼロ) ではない場合は、その値が書式設定用のフィールド幅になります。戻り値は、ユーザーが用意したバッファーの先頭を指すとはかぎりません。 |
char *hex_r (char *buf, int buflen, long num, int width) |
数値を 16 進数の形式で表現した ASCII 文字列のポインタを返す。width が 0 (ゼロ) ではない場合は、その値が書式設定用のフィールド幅になります。戻り値は、ユーザーが用意したバッファーの先頭を指すとはかぎりません。 |
char *dec_r (char *buf, int buflen, long num, int width) |
数値を 10 進数の形式で表現した ASCII 文字列のポインタを返す。width が 0 (ゼロ) ではない場合は、その値が書式設定用のフィールド幅になります。戻り値は、ユーザーが用意したバッファーの先頭を指すとはかぎりません。 |
char *chr_r (char *buf, int buflen, long num, int width) |
文字 chr を含む ASCII 文字列のポインタを返す。width が 0 (ゼロ) ではない場合は、その値と同じ数の空白に続けて chr が格納されます。戻り値は、ユーザーが用意したバッファーの先頭を指すとはかぎりません。 |
char *form_r (char *buf, int buflen, long num, int width) |
sprintf によって書式設定した文字列のポインタを返す。書式文字列 format 以降のすべての引数を使用します。ユーザーが用意したバッファーに、変換後の文字列を収容できるだけの大きさがなければいけません。 |
従来の libC との互換性を確保するために提供されている iostream ライブラリの公開変換ルーチン (oct、hex、dec、chr、form) は、マルチスレッドで使用すると安全ではありません。
libC ライブラリの iostream クラスを使用した、マルチスレッド環境用のアプリケーションを構築するには、-mt オプションを付けてソースコードのコンパイルとリンクを行う必要があります。このオプションを付けると、プリプロセッサに -D_REENTRANT が渡され、リンカーに -lthread が渡されます。
libC と libthread へのリンクを行うには、(-lthread オプションではなく) -mt オプションを使用します。このオプションを使用しないと、ライブラリが正しい順番でリンクされないことがあります。誤って -lthread オプションを使用すると、作成したアプリケーションが正しく機能しない場合があります。
iostream クラスを使用するシングルスレッドアプリケーションについては、コンパイラオプションやリンカオプションは特に必要ありません。オプションを何も指定しなかった場合は、コンパイラは libC ライブラリへのリンクを行います。
iostream ライブラリのマルチスレッドでの安全性には制約があります。これは、マルチスレッド環境で iostream オブジェクトが共有された場合に、iostream を使用するプログラミング手法の多くが安全ではなくなるためです。
マルチスレッドでの安全性を実現するには、エラーの原因になる入出力操作を含んでいる危険領域で、エラーチェックを行う必要があります。エラーが発生したかどうかを確認するには次のようにします。
#include <iostream.h> enum iostate {IOok, IOeof, IOfail}; iostate read_number(istream& istr, int& num) { stream_locker sl(istr, stream_locker::lock_now); istr >> num; if (istr.eof()) return IOeof; if (istr.fail()) return IOfail; return IOok; } |
この例では、stream_locker オブジェクト sl のコンストラクタによって、istream オブジェクト istr がロックされます。このロックは、read_number が終了したときに呼び出される sl のデストラクタによって解除されます。
マルチスレッドでの安全性を実現するには、最後の入力操作と gcount の呼び出しを行う期間に、istream オブジェクトを排他的に使用するスレッドの内部から、gcount 関数を呼び出す必要があります。gcount は次のように呼び出します。
#include <iostream.h> #include <rlocks.h> void fetch_line(istream& istr, char* line, int& linecount) { stream_locker sl(istr, stream_locker::lock_defer); sl.lock(); // ストリーム istr をロック istr >> line; linecount = istr.gcount(); sl.unlock(); // istr のロックを解除 ... } |
この例では、stream_locker クラスの lock メンバー関数を呼び出してから unlock メンバー関数を呼び出すまでが、プログラムの相互排他領域になります。
マルチスレッドでの安全性を実現するには、別々の操作を特定の順番で行う必要があるユーザー定義型用の入出力操作を、危険領域としてロックする必要があります。この入出力操作の例を次に示します。
#include <rlocks.h> #include <iostream.h> class mystream: public istream { // そのほかの定義... int getRecord(char* name, int& id, float& gpa); }; int mystream::getRecord(char* name, int& id, float& gpa) { stream_locker sl(this, stream_locker::lock_now); *this >> name; *this >> id; *this >> gpa; return this->fail() == 0; } |
現行の libC ライブラリに含まれているマルチスレッドで使用しても安全なクラスを使用すると、シングルスレッドアプリケーションの場合でさえも多少のオーバーヘッドが発生します。libC の unsafe_ クラスを使用すると、このオーバーヘッドを回避できます。
次のようにスコープ決定演算子を使用すると、unsafe_ 基底クラスのメンバー関数を実行できます。
cout.unsafe_ostream::put(’4’); |
cin.unsafe_istream::read(buf, len); |
unsafe_ クラスは、マルチスレッドアプリケーションでは安全に使用できません。
unsafe_ クラスを使用する代わりに、cout オブジェクトと cin オブジェクトを unsafe にしてから、通常の操作を行うこともできます。ただし、パフォーマンスが若干低下します。unsafe な cout と cin は、次のように使用します。
#include <iostream.h> // マルチスレッドでの安全性を無効化 cout.set_safe_flag(stream_MT::unsafe_object); // マルチスレッドでの安全性を無効化 cin.set_safe_flag(stream_MT::unsafe_object); cout.put('4'); cin.read(buf, len); |
iostream オブジェクトがマルチスレッドで使用しても安全な場合は、相互排他ロックを行うことで、そのオブジェクトのメンバー変数が保護されます。 しかし、シングルスレッド環境でしか実行されないアプリケーションでは、このロック処理のために、本来なら必要のないオーバーヘッドがかかります。iostream オブジェクトのマルチスレッドでの安全性の有効/無効を動的に切り替えると、パフォーマンスを改善できます。たとえば、iostream オブジェクトのマルチスレッドでの安全性を無効にするには、次のようにします。
fs.set_safe_flag(stream_MT::unsafe_object);// マルチスレッドでの安全性を無効化 .... 各種の入出力操作を実行 |
iostream が複数のスレッド間で共有されないコード領域では、マルチスレッドでの安全性の無効化ストリームであっても、安全に使用できます。たとえば、スレッドが 1 つしかないプログラムや、スレッドごとに非公開の iostream を使用するプログラムでは問題は起きません。
プログラムに同期処理を明示的に挿入すると、iostream が複数のスレッド間で共有される場合にも、マルチスレッドで使用すると安全ではない iostream を安全に使用できるようになります。この例を次に示します。
generic_lock(); fs.set_safe_flag(stream_MT::unsafe_object); ... 各種の入出力操作を実行 generic_unlock(); |
ここで、generic_lock 関数と generic_unlock 関数は、相互排他ロック (mutex)、セマフォー、読み取り/書き込みロックといった基本型を使用する同期機能であれば、何でもかまいません。
この目的のためには、libC ライブラリの stream_locker クラスを使用すると便利です。
詳細は、「11.4.5 オブジェクトのロック」を参照してください。
この節では、iostream ライブラリをマルチスレッドで使用しても安全にするために行われたインタフェースの変更内容について説明します。
libC インタフェースに追加された新しいクラスを次の表に示します。
stream_MT stream_locker unsafe_ios unsafe_istream unsafe_ostream unsafe_iostream unsafe_fstreambase unsafe_strstreambase |
iostream インタフェースに追加された新しいクラス階層を次の表に示します。
class streambuf: public stream_MT {...}; class unsafe_ios {...}; class ios: virtual public unsafe_ios, public stream_MT {...}; class unsafe_fstreambase: virtual public unsafe_ios {...}; class fstreambase: virtual public ios, public unsafe_fstreambase {...}; class unsafe_strstreambase: virtual public unsafe_ios {...}; class strstreambase: virtual public ios, public unsafe_strstreambase {...}; class unsafe_istream: virtual public unsafe_ios {...}; class unsafe_ostream: virtual public unsafe_ios {...}; class istream: virtual public ios, public unsafe_istream {...}; class ostream: virtual public ios, public unsafe_ostream {...}; class unsafe_iostream: public unsafe_istream, public unsafe_ostream {...}; |
iostream インタフェースに追加された新しい関数を次の表に示します。
class streambuf { public: int sgetc_unlocked(); void sgetn_unlocked(char *, int); int snextc_unlocked(); int sbumpc_unlocked(); void stossc_unlocked(); int in_avail_unlocked(); int sputbackc_unlocked(char); int sputc_unlocked(int); int sputn_unlocked(const char *, int); int out_waiting_unlocked(); protected: char* base_unlocked(); char* ebuf_unlocked(); int blen_unlocked(); char* pbase_unlocked(); char* eback_unlocked(); char* gptr_unlocked(); char* egptr_unlocked(); char* pptr_unlocked(); void setp_unlocked(char*, char*); void setg_unlocked(char*, char*, char*); void pbump_unlocked(int); void gbump_unlocked(int); void setb_unlocked(char*, char*, int); int unbuffered_unlocked(); char *epptr_unlocked(); void unbuffered_unlocked(int); int allocate_unlocked(int); }; class filebuf: public streambuf { public: int is_open_unlocked(); filebuf* close_unlocked(); filebuf* open_unlocked(const char*, int, int = filebuf::openprot); filebuf* attach_unlocked(int); }; class strstreambuf: public streambuf { public: int freeze_unlocked(); char* str_unlocked(); }; unsafe_ostream& endl(unsafe_ostream&); unsafe_ostream& ends(unsafe_ostream&); unsafe_ostream& flush(unsafe_ostream&); unsafe_istream& ws(unsafe_istream&); unsafe_ios& dec(unsafe_ios&); unsafe_ios& hex(unsafe_ios&); unsafe_ios& oct(unsafe_ios&); char* dec_r (char* buf, int buflen, long num, int width) char* hex_r (char* buf, int buflen, long num, int width) char* oct_r (char* buf, int buflen, long num, int width) char* chr_r (char* buf, int buflen, long chr, int width) char* str_r (char* buf, int buflen, const char* format, int width = 0); char* form_r (char* buf, int buflen, const char* format,...) |
マルチスレッドアプリケーションでの大域データと静的データは、スレッド間で安全に共有されません。スレッドはそれぞれ個別に実行されますが、同じプロセス内のスレッドは、大域オブジェクトと静的オブジェクトへのアクセスを共有します。このような共有オブジェクトをあるスレッドで変更すると、その変更が同じプロセス内のほかのスレッドにも反映されるため、状態を保つことが難しくなります。C++ では、クラスオブジェクト (クラスのインスタンス) の状態は、メンバー変数の値が変わると変化します。そのため、共有されたクラスオブジェクトは、ほかのスレッドからの変更に対して脆弱です。
マルチスレッドアプリケーションで iostream ライブラリを使用し、iostream.h をインクルードすると、デフォルトでは標準ストリーム (cout、cin、cerr、clog) が大域的な共有オブジェクトとして定義されます。iostream ライブラリはマルチスレッドで使用しても安全なので、iostream オブジェクトのメンバー関数の実行中は、共有オブジェクトの状態が、ほかのスレッドからのアクセスや変更から保護されます。ただし、オブジェクトがマルチスレッドで使用しても安全なのは、そのオブジェクトの公開メンバー関数が実行されている間だけです。次に例を示します。
int c; cin.get(c); |
このコードを使用して、スレッド A が get バッファーの次の文字を取り出し、バッファーポインタを更新したとします。ところが、スレッド A が、次の命令で再び get を呼び出したとしても、シーケンスのその次の文字が返される保証はありません。なぜなら、スレッド A の 2 つの get の呼び出しの間に、スレッド B からも別の get が呼び出される可能性があるからです。
このような共有オブジェクトとマルチスレッド処理の問題に対処する方法については、「11.4.5 オブジェクトのロック」を参照してください。
iostream オブジェクトを使用した場合に、一続きの入出力操作をマルチスレッドで使用しても安全にしなければならない場合がよくあります。次は
cout << " Error message:" << errstring[err_number] << "\n"; |
このコードでは、cout ストリームオブジェクトの 3 つのメンバー関数が実行されます。cout は共有オブジェクトなので、マルチスレッド環境では、この操作全体を危険領域として不可分的に (連続して) 実行しなければなりません。iostream クラスのオブジェクトに対する一続きの操作を不可分的に実行するには、何らかのロック処理が必要です。
iostream オブジェクトをロックできるように、libC ライブラリに新しく stream_locker クラスが追加されています。stream_locker クラスの詳細については、「11.4.5 オブジェクトのロック」を参照してください。
共有オブジェクトとマルチスレッド処理の問題に対処するもっとも簡単な方法は、iostream オブジェクトをスレッドの局所的なオブジェクトにして、問題そのものを解消してしまうことです。次に例を示します。
スレッドのエントリ関数の中でオブジェクトを局所的に宣言する。
スレッド固有データの中でオブジェクトを宣言する。スレッド固有データの使用法については、thr_keycreate(3T) のマニュアルページを参照してください。
ストリームオブジェクトを特定のスレッド専用にする。このオブジェクトスレッドは、慣例により非公開 (private) になります。
ただし、デフォルトの共有標準ストリームオブジェクトを初めとして、多くの場合はオブジェクトをスレッドの局所的なオブジェクトにすることはできません。 そのため、別の手段が必要です。
iostream クラスのオブジェクトに対する一続きの操作を不可分的に実行するには、何らかのロック処理が必要です。ただし、ロック処理を行うと、シングルスレッドアプリケーションの場合でさえも、オーバーヘッドが多少増加します。ロック処理を追加する必要があるか、それとも iostream オブジェクトをスレッドの非公開オブジェクトにすればよいかは、アプリケーションで採用しているスレッドモデル (独立スレッドと連携スレッドのどちらを使用しているか) によって決まります。
スレッドごとに別々の iostream オブジェクトを使用してデータを入出力する場合は、それぞれの iostream オブジェクトが、該当するスレッドの非公開オブジェクトになります。ロック処理の必要はありません。
複数のスレッドを連携させる (これらのスレッドの間で、同じ iostream オブジェクトを共有させる) 場合は、その共有オブジェクトへのアクセスの同期をとる必要があり、何らかのロック処理によって、一続きの操作を不可分的にする必要があります。
iostream ライブラリには、iostream オブジェクトに対する一続きの操作をロックするための stream_locker クラスが含まれています。これにより、iostream オブジェクトのロックを動的に切り換えることで生じるオーバーヘッドを最小限にできます。
stream_locker クラスのオブジェクトを使用すると、ストリームオブジェクトに対する一続きの操作を不可分的にできます。たとえば、次の例を考えてみましょう。 このコードは、ファイル内の位置を特定の場所まで移動し、その後続のデータブロックを読み込みます。
#include <fstream.h> #include <rlocks.h> void lock_example (fstream& fs) { const int len = 128; char buf[len]; int offset = 48; stream_locker s_lock(fs, stream_locker::lock_now); .....// ファイルを開く fs.seekg(offset, ios::beg); fs.read(buf, len); } |
この例では、stream_locker オブジェクトのコンストラクタが実行されてから、デストラクタが実行されるまでが、一度に 1 つのスレッドしか実行できない相互排他領域になります。デストラクタは、lock_example 関数が終了したときに呼び出されます。この stream_locker オブジェクトにより、ファイル内の特定のオフセットへの移動と、ファイルからの読み込みの連続的な (不可分的な) 実行が保証され、ファイルからの読み込みを行う前に、別のスレッドによってオフセットが変更されてしまう可能性がなくなります。
stream_locker オブジェクトを使用して、相互排他領域を明示的に定義することもできます。次の例では、入出力操作と、そのあとで行うエラーチェックを不可分的にするために、stream_locker オブジェクトのメンバー関数、lock と unlock を呼び出しています。
{ ... stream_locker file_lck(openfile_stream, stream_locker::lock_defer); .... file_lck.lock(); // openfile_stream をロック openfile_stream << "Value: " << int_value << "\n"; if(!openfile_stream) { file_error("Output of value failed\n"); return; } file_lck.unlock(); // openfile_stream のロックを解除 } |
詳細については、stream_locker(3CC4) のマニュアルページを参照してください。
iostream クラスから新しいクラスを派生させて、機能を拡張または特殊化できます。マルチスレッド環境で、これらの派生クラスからインスタンス化したオブジェクトを使用する場合は、その派生クラスがマルチスレッドで使用しても安全でなければなりません。
マルチスレッドで使用しても安全なクラスを派生させる場合は、次のことに注意する必要があります。
クラスオブジェクトの内部状態を複数のスレッドによる変更から保護し、そのオブジェクトをマルチスレッドで使用しても安全にします。そのためには、公開および限定公開のメンバー関数に含まれているメンバー変数へのアクセスを、相互排他ロックで直列化します。
マルチスレッドで使用しても安全な基底クラスのメンバー関数を、一続きに呼び出す必要がある場合は、それらの呼び出しを stream_locker オブジェクトを使用して不可分にします。
stream_locker オブジェクトで定義した危険領域の内部では、streambuf クラスの _unlocked メンバー関数を使用して、ロック処理のオーバーヘッドを防止します。
streambuf クラスの公開仮想関数を、アプリケーションから直接呼び出す場合は、それらの関数をロックします。該当する関数は、次のとおりです。xsgetn、underflow、pbackfail、xsputn、overflow、seekoff、seekpos
ios クラスの iword メンバー関数と pword メンバー関数を使用して、ios オブジェクトの書式設定状態を拡張します。ただし、複数のスレッドが iword 関数や pword 関数の同じ添字を共有している場合は、問題が発生することがあります。これらのスレッドをマルチスレッドで使用しても安全にするには、適切なロック機能を使用する必要があります。
メンバー関数のうち、char 型よりも大きなサイズのメンバー変数値を返すものをロックします。
複数のスレッドの間で共有される iostream オブジェクトを削除するには、サブスレッドがそのオブジェクトの使用を終えていることを、メインスレッドで確認する必要があります。共有オブジェクトを安全に破棄する方法を次に示します。
#include <fstream.h> #include <thread.h> fstream* fp; void *process_rtn(void*) { // fp を使用するサブスレッドの本体... } void multi_process(const char* filename, int numthreads) { fp = new fstream(filename, ios::in); // スレッドを生成する前に // fstream オブジェクトを生成 // スレッドを生成 for (int i=0; i<numthreads; i++) thr_create(0, STACKSIZE, process_rtn, 0, 0, 0); ... // スレッドが終了するまで待機 for (int i=0; i<numthreads; i++) thr_join(0, 0, 0); delete fp; // すべてのスレッドが終了した fp = NULL; // あとで fstream オブジェクトを削除 } |
ここでは、libC ライブラリの iostream オブジェクトを安全な方法で使用するマルチスレッドアプリケーションの例を示します。
このアプリケーションは、最大で 255 のスレッドを生成します。それぞれのスレッドは、別々の入力ファイルを 1 行ずつ読み込み、標準出力ストリーム cout を介して共通の出力ファイルに書き出します。この出力ファイルは、すべてのスレッドから共有されるため、出力操作がどのスレッドから行われたかを示す値をタグとして付けます。
// タグ付きスレッドデータの生成 // 出力ファイルに次の形式で文字列を書き出す // <タグ><データ文字列>\n // ここで、<タグ> は unsigned char 型の整数値 // このアプリケーションで最大 255 のスレッドを実行可能 // <データ文字列> は任意のプリント可能文字列 // <タグ> は char 型として書き出される整数値なので、 // 出力ファイルの内容を参照するには、次のように od を使用する // od -c out.file |more #include <stdlib.h> #include <stdio.h> #include <iostream.h> #include <fstream.h> #include <thread.h> struct thread_args { char* filename; int thread_tag; }; const int thread_bufsize = 256; // それぞれのスレッドのエントリルーチン void* ThreadDuties(void* v) { // このスレッドの引数を取得 thread_args* tt = (thread_args*)v; char ibuf[thread_bufsize]; // 入力ファイルを開く ifstream instr(tt->filename); stream_locker lockout(cout, stream_locker::lock_defer); while(1) { // 1 行ずつ読み込む instr.getline(ibuf, thread_bufsize - 1, ’\n’); if(instr.eof()) break; // cout ストリームをロックし、入出力操作を不可分にする lockout.lock(); // 行にタグを付けて cout に送出する cout << (unsigned char)tt->thread_tag << ibuf << "\n"; lockout.unlock(); } return 0; } int main(int argc, char** argv) { // argv:1 + 各スレッドのファイル名リスト if(argc < 2) { cout << “usage: " << argv[0] << " <files..>\n"; exit(1); } int num_threads = argc - 1; int total_tags = 0; // thread_id の配列 thread_t created_threads[thread_bufsize]; // スレッドのエントリ配列に渡す引数配列 thread_args thr_args[thread_bufsize]; int i; for(i = 0; i < num_threads; i++) { thr_args[i].filename = argv[1 + i]; // スレッドにタグを割り当てる (255 以下の値) thr_args[i].thread_tag = total_tags++; // スレッドを生成する thr_create(0, 0, ThreadDuties, &thr_args[i], THR_SUSPENDED, &created_threads[i]); } for(i = 0; i < num_threads; i++) { thr_continue(created_threads[i]); } for(i = 0; i < num_threads; i++) { thr_join(created_threads[i], 0, 0); } return 0; } |