GCCを使ったマルチスレッド・プログラミングの基本 |
簡単にいえば,プロセスは固有のメモリ空間・固有のスタック上で動作し,スレッドは共有のメモリ空間・固有のスタック上で動作するものです.具体的な使用例をリスト1に示します.
まずはプロセスの場合です.システム・コールfork()で,親プロセスとまったく同じコピーを作成し,親には子のプロセスIDを,子にはゼロを返します.このコードでプロセスを生成できます.これは単純に別プロセスでxineを起動しているだけです(図2).
親プロセスであるtest200のPIDは18727,子プロセスとして実行したxineは18728です.
一方,スレッドの場合はどうなるでしょうか? POSIXスレッドを生成してみます.リスト2は単純にスレッドを実行するプログラムです.
通常は,このソースをコンパイルするには次のようにすればOKです.“-lpthread”でライブラリを指定します.
$ gcc -lpthread test201.c -o test201
$ ./test201
メインスレッドのスレッドID=1074008704
スレッド1のスレッドID=1082399936
スレッド2のスレッドID=1090788416
スレッド1を実行中
スレッド2を実行中
スレッド1を実行中
スレッド2を実行中
スレッド1を実行中
スレッド2を実行中
$
そして,非同期イベントが発生したときにフレームの巻き戻しを行うことが可能になります.
このように別個のスレッドIDが付加されています.暗黙に実行されているメイン・スレッドに一つ,各スレッドに一つずつです.
リスト3のコードで関数を実行するものが生成するアセンブラ・コードと比較してみましょう(リスト4,リスト5).関数では「call thread_function1」で呼び出していますが,スレッド処理ではthread_function1のアドレスをpthread _createに渡しています.
次に「スレッドは共有のメモリ空間を使用する」ことを実証してみます.リスト6を参照してください.実行結果は次のとおりです.
$ gcc -lpthread test202.c -o test202
$ ./test202
メインスレッドのスレッドID=1074008704
スレッド1のスレッドID=1082399936
スレッド2のスレッドID=1090788416
スレッド1を開始します Gintの初期値は=
1000
スレッド2を開始します Gintの初期値は=
2000
スレッド1が終了しました Gintの値は=
17134
スレッド2が終了しました Gintの値は=
21333
$
計算結果はめちゃくちゃです.この処理はスレッド1の優先度を上げているので先に終わりますが,スレッド2の優先度を上げても同じことです.
リスト7で検証してみましょう.実行結果は次のとおりです.
$ ./test203
メインスレッドのスレッドID=1074008704
スレッド1のスレッドID=1082399936
スレッド2のスレッドID=1090788416
スレッド1を開始します Gintの初期値は=
1000
スレッド2を開始します Gintの初期値は=
2000
スレッド2が終了しました Gintの値は=
21654
スレッド1が終了しました Gintの値は=
21866
$
この処理はスレッド2の優先度を上げているので先に終わりましたが,計算結果がおかしいのは同じことです.
●mutexの使用
それを防止するために変数Gintをmutexを使用してロックします.具体的なコーディングはリスト8のようになります.実行結果は次のとおりです.
$ gcc -lpthread test204.c -o test204
$ ./test204
メインスレッドのスレッドID=1074008704
スレッド1のスレッドID=1082399936
スレッド1を開始します Gintの初期値は=
1000
スレッド1が終了しました Gintの値は=
11000
スレッド2のスレッドID=1090788416
スレッド2を開始します Gintの初期値は=
2000
スレッド2が終了しました Gintの値は=
12000
$
この場合,スレッド2の優先度が高くても先にGintを取ってロックしたほうが優先になります.よってスレッド1が先に実行されます.
早い者勝ちでつかんだ変数をロックして,処理が終わったらアンロックしています.これがmutexを使用した排他処理です.計算結果は意図した値が保持されています.
スレッドのメモリ空間はプロセス内で共有されています.そのため静的な変数は同じプロセスに属するすべてのスレッドから操作可能となります.自動変数はスタック上にとられるためスレッドごとに固有に確保されることになります.
メモリの確保をスタックにしたい場合,スレッド処理ではallocaを使います.その際はオプションの指定でスタック・オーバフローをチェックしましょう.-fstack-checkを指定します.これは連載第5回で説明しています.
また,各スレッドが同じメモリ空間を共有しているということは,複数のスレッドから使用する変数の操作を行う場合には注意しなくてはなりません.そこでいちばん使われる手法が,先に述べたmutexを使用してロックすることです.
● スレッド実行権限の譲渡
mutexを使用しなくても,リスト9のような処理もできます.実行結果は次のとおりです.
gcc -lpthread test205.c -o test205
$ ./test205
メインスレッドのスレッドID=1074008704
スレッド1のスレッドID=1082399936
スレッド2のスレッドID=1090788416
スレッド2を開始します Gintの初期値は=
2000
スレッド2が終了しました Gintの値は=
12000
スレッド1を開始します Gintの初期値は=
1000
スレッド1が終了しました Gintの値は=
11000
この場合,結果こそ正しくなっていますが,このような処理では無理があると思います.sched_yield()を実行するたびに他スレッドに実行権限を渡します.結果,自スレッドは実質的にスリープしています.
● セマフォの使用
セマフォと呼ばれるプログラミング手法があります.もともとはプロセス間の実行順序などを管理するためにあるものですが,しっかり管理しないと混乱を招きます.多数の開発者がいるプロジェクトでは,管理者が管理しなければまともなシステムが構築できなくなります.
スレッドの場合は,同一プロセス内に限って使用できるセマ フォを使うことで,閉じている状態にできます.リスト10のような方法で実現できます.実行結果は次のとおりです.
$ gcc -lpthread test206.c -o test206
$ ./test206
メインスレッドのスレッドID=1074008704
スレッド2のスレッドID=1090788416
スレッド2を開始します Gintの初期値は=
2000
スレッド2が終了しました Gintの値は=
12000
スレッド1のスレッドID=1082399936
スレッド1を開始します Gintの初期値は=
1000
スレッド1が終了しました Gintの値は=
11000
$
結果も正しく,安全確実にスレッドの実行順序をコントロールできました.
ほかにも条件変数を設定し,シグナルや時刻指定でスレッドの実行を管理する方法があります.コードを実装すれば,容易にネットワークからのスレッドの呼び起こし,そのほかキーボードやマウス・イベントでも可能になります.
おわりに
このようにGCCでもまったく問題なくマルチスレッド処理ができることがわかったと思います.
現実には実プロセッサが複数なければ,カーネルのタイムシェアで疑似的にマルチスレッドが実行されているだけですが,カーネル2.6を使い,Xeonプロセッサを複数使ったPCで,マルチスレッド・プログラミングを行えば,より効率の良いコードが書けることと思います.
次回は「最適化オプション」の補足を行います.