並列分散ソフトウェア
電子・情報工学系
新城 靖
<yas@is.tsukuba.ac.jp>
このページは、次の URL にあります。
http://www.hlla.is.tsukuba.ac.jp/~yas/sie/pdsoft-2000/2001-01-18
あるいは、次のページから手繰っていくこともできます。
http://www.hlla.is.tsukuba.ac.jp/~yas/sie/
http://www.is.tsukuba.ac.jp/~yas/index-j.html
http://www.hlla.is.tsukuba.ac.jp/~yas/index-j.html
SunOS 5.6 (sakura) でセマフォを使う時には、-lrt ではなくて、
-lposix4 をつける。
SunOS 5.6 (sakura) には、pthread_setconcurrency() は使えない。
代りに thr_setconcurrency()を使う。使い方は、まったく同じである。
もともと Sun 独自の機能であったものが、標準に採り入れられた。SunOS 5.6
でpthread_setconcurrency() を含むソースをコンパイルする時には、
Cコンパイラのコマンドラインで次のようなマクロを定義するとよい。
ソース・プログラムを修正するなら、次のようなマクロを定義するとよい。-Dpthread_setconcurrency=thr_setconcurrency
自分で定義してリンクしてもよい。#define pthread_setconcurrency thr_setconcurrency
軽量プロセスというと、内部にループを含むような語感がある。
マルチスレッドプログラミングは、非常に難しい。
どこでスレッドを使うべきか。
Pthread を利用したプログラムを書く時には、次のようなヘッダ・ファイルを 読み込む。
#include <pthread.h>
Solaris (Unix International系)でのコンパイルとリンク。
-D_REENTRANTと-lpthread を付ける。
---------------------------------------------------------------------- % cc -D_REENTRANT -o create create.c -lpthread% ./create
... %
----------------------------------------------------------------------
SGI (O2, Origin) でのコンパイルとリンク。-lpthread を付ける。
---------------------------------------------------------------------- % cc -o create create.c -lpthread% ./create
... %
----------------------------------------------------------------------

図? fork-joinモデルの実現
後で join する必要がない時には、pthread_detach() を使って切り離す。 (joinしなくてもゾンビが残らない。)
図? スレッドの生成とjoin
----------------------------------------------------------------------
1:
2: /*
3: create-join-2.c -- スレッドを2つ作るプログラム
4: このファイルは次の場所にあります。
5: http://www.hlla.is.tsukuba.ac.jp/~yas/sie/pdsoft-2000/examples/create-join-2.c
6: wget コマンドでコピーできます。
7: $Header: /home/hlla/yas/cvs/sie-pdsoft-2000-examples/create-join-2.c,v 1.1 2001/01/06 22:05:52 yas Exp $
8: Start: 1999/01/18 20:49:16
9: */
10:
11: #include <pthread.h>
12:
13: void func1( int x );
14: void func2( int x );
15:
16: main()
17: {
18: pthread_t t1 ;
19: pthread_t t2 ;
20: pthread_create( &t1, NULL, (void *)func1, (void *)10 );
21: pthread_create( &t2, NULL, (void *)func2, (void *)20 );
22: printf("main()\n");
23: pthread_join( t1, NULL );
24: pthread_join( t2, NULL );
25: }
26:
27: void func1( int x )
28: {
29: int i ;
30: for( i = 0 ; i<3 ; i++ )
31: {
32: printf("func1( %d ): %d \n",x, i );
33: }
34: }
35:
36: void func2( int x )
37: {
38: int i ;
39: for( i = 0 ; i<3 ; i++ )
40: {
41: printf("func2( %d ): %d \n",x, i );
42: }
43: }
----------------------------------------------------------------------
実行例。
この例では、次の3つのスレッドが作られる。---------------------------------------------------------------------- % wget http://www.hlla.is.tsukuba.ac.jp/~yas/sie/pdsoft-2000/examples/create-join-2.c% wget http://www.hlla.is.tsukuba.ac.jp/~yas/sie/pdsoft-2000/examples/Makefile
% make create-join-2
gcc -D_REENTRANT -g -c create-join-2.c -o create-join-2.o gcc -D_REENTRANT -g -o create-join-2 create-join-2.o -lpthread % ./create-join-2
main() func1( 10 ): 0 func1( 10 ): 1 func1( 10 ): 2 func2( 20 ): 0 func2( 20 ): 1 func2( 20 ): 2 %
----------------------------------------------------------------------
func2 から作られたスレッド t2
func1 から作られたスレッド t1
pthread_create() により作られる。
どういう順序で実行されるかは、決まっていない。 決まっていない。スレッドは、もともと順番を決めないような処理、 非同期的(asynchronous) な処理を表現するためのもの。どうしても他のスレッドと同期を行なう必要が 出てきた時には、mutex や条件変数といった同期機能を使う。
pthread_create()で指定された関数からリターンすると、そ
のスレッドが終了する。pthread_exit() を呼び出してもよい。
ただし、
初期スレッド
が終了すると、プロセス全体が終了する。
exit() システムコールを呼び出しても終了する。
プロセスの場合、ファイル、端末、ウインドウ、主記憶、CPU時間。
主記憶やCPU時間といった資源は、横取りしても平気なので、あまり問題になりない。
スレッドの場合は、プログラミング言語の変数。
変更されない変数の値を読むだけなら、特に問題は起きない。それ意外の時、 特に、複数のスレッドで値を読み書きする時に問題が起きる。
----------------------------------------------------------------------
1:
2: /*
3: * mutex-nolock.c -- 共有資源をロックなしでアクセスするプログラム
4: * このファイルは次の場所にあります。
5: * http://www.hlla.is.tsukuba.ac.jp/~yas/sie/pdsoft-2000/examples/mutex-nolock.c
6: * wget コマンドでコピーできます。
7: * $Header: /home/hlla/yas/cvs/sie-pdsoft-2000-examples/mutex-nolock.c,v 1.1 2001/01/06 23:57:04 yas Exp $
8: *Start: 1999/01/25 22:13:15
9: */
10:
11: #include <pthread.h>
12:
13: void thread_A(), thread_B();
14: int shared_resource ;
15:
16: main() {
17: pthread_t t1 ;
18: pthread_t t2 ;
19: shared_resource = 0 ;
20: pthread_setconcurrency( 2 );
21: pthread_create( &t1, NULL, (void *)thread_A, 0 );
22: pthread_create( &t2, NULL, (void *)thread_B, 0 );
23: pthread_join( t1, NULL );
24: pthread_join( t2, NULL );
25: printf("main(): shared_resource == %d\n", shared_resource );
26: }
27:
28: void thread_A()
29: {
30: int i, x ;
31: for( i = 0 ; i<1000000 ; i++ )
32: {
33: x = shared_resource ;
34: x = x + 1 ;
35: shared_resource = x ;
36: }
37: }
38:
39: void thread_B() {
40: int i, x ;
41: for( i = 0 ; i<1000000 ; i++ ) {
42: x = shared_resource ;
43: x = x + 1 ;
44: shared_resource = x ;
45: }
46: }
----------------------------------------------------------------------
共有メモリ型マルチプロセッサでの実行結果(sakura,icho)。
---------------------------------------------------------------------- % wget http://www.hlla.is.tsukuba.ac.jp/~yas/sie/pdsoft-2000/examples/mutex-nolock.c% wget http://www.hlla.is.tsukuba.ac.jp/~yas/sie/pdsoft-2000/examples/Makefile
% make mutex-nolock
gcc -D_REENTRANT -g -c mutex-nolock.c -o mutex-nolock.o gcc -D_REENTRANT -g -o mutex-nolock mutex-nolock.o -lpthread %
% ./mutex-nolock
main(): shared_resource == 1001421 % ./mutex-nolock
main(): shared_resource == 1002030 % ./mutex-nolock
main(): shared_resource == 1015670 %
----------------------------------------------------------------------
thread_A(), thread_B()と2つのスレッドが作
られている。整数型の変数 shared_resource は、その2つの
スレッドの両方からアクセスされる。2つのスレッドで合計2000000 増える予
定だが、実際に実行すると、そうはならない。実行する度に答えが違う。
このプログラムが共有メモリ型マルチプロセッサで動いているとして、動きを 考えてえる。
---------------------------------------------------------------------- thread_A() thread_B() : : : : 33: x = shared_resource ; 42: x = shared_resource ; 34: x = x + 1 ; 43: x = x + 1 ; 35: shared_resource = x ; 44: shared_resource = x ; : : : : ----------------------------------------------------------------------
shared_resource++と書いてもだめ。複数の機械語命令に分割
されるので。
相互排除(mutual exclusion): ある資源をアクセスできるスレッドの数を 多くても1つにする。
プログラムの字面上、相互排除が必要な部分を 際どい部分(critical section) ( クリティカルセクション ) という。
Pthread では、相互排除を行なうために、 mutex という仕組みがある。次のようにして、相互排除を行ないたい部分を lock と unlock で囲む。
pthread_mutex_t mutex1 ;
:
:
pthread_mutex_lock( &murex1 );
<相互排除したい部分(際どい部分)>
pthread_mutex_unlock( &mutex1 );
ロックなしのプログラムを、mutex を使っ て書き直したもの。
----------------------------------------------------------------------
1: /*
2: * mutex-lock.c -- 共有資源をロックしながらアクセスするプログラム
3: * このファイルは次の場所にあります。
4: * http://www.hlla.is.tsukuba.ac.jp/~yas/sie/pdsoft-2000/examples/mutex-lock.c
5: * wget コマンドでコピーできます。
6: * $Header: /home/hlla/yas/cvs/sie-pdsoft-2000-examples/mutex-lock.c,v 1.1 2001/01/07 00:08:54 yas Exp $
7: *Start: 1999/01/25 22:13:15
8: */
9:
10: #include <pthread.h>
11:
12: void thread_A(), thread_B();
13: int shared_resource ;
14: pthread_mutex_t mutex1 ;
15:
16: main() {
17: pthread_t t1 ;
18: pthread_t t2 ;
19: shared_resource = 0 ;
20: pthread_mutex_init( &mutex1, NULL );
21: pthread_setconcurrency( 2 );
22: pthread_create( &t1, NULL, (void *)thread_A, 0 );
23: pthread_create( &t2, NULL, (void *)thread_B, 0 );
24: pthread_join( t1, NULL );
25: pthread_join( t2, NULL );
26: printf("main(): shared_resource == %d\n", shared_resource );
27: }
28:
29: void thread_A()
30: {
31: int i, x ;
32: for( i = 0 ; i<1000000 ; i++ )
33: {
34: pthread_mutex_lock( &mutex1 );
35: x = shared_resource ;
36: x = x + 1 ;
37: shared_resource = x ;
38: pthread_mutex_unlock( &mutex1 );
39: }
40: }
41:
42: void thread_B() {
43: int i, x ;
44: for( i = 0 ; i<1000000 ; i++ ) {
45: pthread_mutex_lock( &mutex1 );
46: x = shared_resource ;
47: x = x + 1 ;
48: shared_resource = x ;
49: pthread_mutex_unlock( &mutex1 );
50: }
51: }
----------------------------------------------------------------------
---------------------------------------------------------------------- % wget http://www.hlla.is.tsukuba.ac.jp/~yas/sie/pdsoft-2000/examples/mutex-loc.c% wget http://www.hlla.is.tsukuba.ac.jp/~yas/sie/pdsoft-2000/examples/Makefile
% make mutex-lock
gcc -D_REENTRANT -g -c mutex-lock.c -o mutex-lock.o gcc -D_REENTRANT -g -o mutex-lock mutex-lock.o -lpthread % ./mutex-lock
main(): shared_resource == 2000000 % ./mutex-lock
main(): shared_resource == 2000000 % ./mutex-lock
main(): shared_resource == 2000000 %
----------------------------------------------------------------------
このプログラムが共有メモリ型マルチプロセッサで動いているとして、動きを 考えてえる。
---------------------------------------------------------------------- thread_A() thread_B() : : 34: pthread_mutex_lock( &mutex1 ); : 45: pthread_mutex_lock( &mutex1 ); 35: x = shared_resource ; <他のスレッドが実行中なので 36: x = x + 1 ; この状態でしばらく待たされる> 37: shared_resource = x ; : 38: pthread_mutex_unlock( &mutex1 ); : : <実行再開> : 46: x = shared_resource ; 47: x = x + 1 ; 48: shared_resource = x ; 49: pthread_mutex_unlock( &mutex1 ); ----------------------------------------------------------------------後から来た
thread_B() は、他のスレッドが実行中は、待たさ
れる。
2つのスレッドの間には、バッファを置く。---------------------------------------------------------------------- thread_A | thread_B ----------------------------------------------------------------------
バッファが空の時、YHM_thread_B() は、YHM_thread_A() が何かデー タをバッファに入れるのを待つ。バッファがいっぱいの時、YHM_thread_A() は、YHM_thread_B() がバッファから何かデータを取り出すのを待つ。
条件変数(condition variable) で、ある条件が生じたことを待つ。条件変数の操作:
----------------------------------------------------------------------
/*
* condv-buffer.c -- 条件変数を使った環状バッファ
* このファイルは次の場所にあります。
* http://www.hlla.is.tsukuba.ac.jp/~yas/sie/pdsoft-2000/examples/condv-buffer.c
* wget コマンドでコピーできます。
$Header: /home/hlla/yas/cvs/sie-pdsoft-2000-examples/condv-buffer.c,v 1.1 2001/01/07 00:23:03 yas Exp $
Start: 1999/01/25 22:13:15
/
#include <pthread.h>
void thread_A(), thread_B();
#define BUFFER_SIZE 4 /* バッファの大きさ */
struct buffer
{
int rp ; /* 読み出す位置 */
int wp ; /* 書き込む位置 */
int data[BUFFER_SIZE]; /* データを保存する場所 */
pthread_mutex_t mutex ; /* この構造体の相互排除のための mutex */
pthread_cond_t not_full ; /* バッファが一杯ではない状態を待つための条件変数 */
pthread_cond_t not_empty ; /* バッファが空ではない状態を待つための条件変数 */
};
void put( struct buffer *b,int x )
{
pthread_mutex_lock( &b->mutex );
loop: if( ((b->wp+1)%BUFFER_SIZE) == b->rp )
{
pthread_cond_wait( &b->not_full,&b->mutex );
goto loop;
}
b->data[ b->wp++ ] = x ;
if( b->wp >= BUFFER_SIZE )
b->wp = 0 ;
pthread_cond_signal( &b->not_empty );
pthread_mutex_unlock( &b->mutex );
}
int get( struct buffer *b )
{
int x ;
pthread_mutex_lock( &b->mutex );
loop: if( b->rp == b->wp )
{
pthread_cond_wait( &b->not_empty,&b->mutex );
goto loop;
}
x = b->data[ b->rp++ ] ;
if( b->rp >= BUFFER_SIZE )
b->rp = 0 ;
pthread_cond_signal( &b->not_full );
pthread_mutex_unlock( &b->mutex );
return( x );
}
main()
{
pthread_t t1 ;
pthread_t t2 ;
struct buffer *b ;
if( (b = (struct buffer *)malloc(sizeof(struct buffer))) == NULL )
{
perror("no memory for struct buffer\n");
exit( -1 );
}
b->rp = 0 ;
b->wp = 0 ;
pthread_mutex_init( &b->mutex, NULL );
pthread_cond_init( &b->not_full,NULL );
pthread_cond_init( &b->not_empty,NULL );
pthread_create( &t1, NULL, (void *)thread_A, (void *)b );
pthread_create( &t2, NULL, (void *)thread_B, (void *)b );
pthread_join( t1, NULL );
pthread_join( t2, NULL );
}
void thread_A( struct buffer *b ) /* producer */
{
int i,x ;
for( i = 0 ; i<10 ; i++ )
{
x = i ;
printf("thread_A(): put( %d )\n",x );
put( b,x );
}
}
void thread_B( struct buffer *b ) /* consumer */
{
int i, x ;
for( i = 0 ; i<10 ; i++ )
{
x = get( b );
printf("thread_B(): get() %d.\n", x );
}
}
----------------------------------------------------------------------
put() は、バッファにデータを追加する時に使う手続き。
基本的には、入口で pthread_mutex_lock() し、
出口で pthread_mutex_unlock() する。
バッファが一杯の時には、条件変数b->not_full で、
一杯でないという条件になるまで待つ。
待っている間は、mutex のロックは解除される。
pthread_cond_wait() からリターンして来る時には、もう一度
ロックされた状態に戻るが、待っている間に、他の変数
(rp,wp,data)が書き換えられている可能性があるので、もう一
度最初から調べる。
get() は、バッファからデータを取り出す時に使う手
続き。put()とほぼ対称形。バッファが空の時に、wait
し、バッファがもはや一杯ではないことをsignal する。
thread_A() は、10回バッファにデータを書き込むスレッド。
thread_B() は逆に、10回バッファからデータを読み出すス
レッド。
整数を1つバッファに書き込むだけでロック/アンロックを行なっていると、 実際の並列処理では重たい。ロックの回数を減らすために、ダブルバッファリ ングと呼ばれる技術がよく使われる。読み手と書き手で別々にバッファをもう け、1つのバッファの処理をしている間は、ロックを行なわない。---------------------------------------------------------------------- % wget http://www.hlla.is.tsukuba.ac.jp/~yas/sie/pdsoft-2000/examples/condv-buffer.c% wget http://www.hlla.is.tsukuba.ac.jp/~yas/sie/pdsoft-2000/examples/Makefile
% make condv-buffer
gcc -D_REENTRANT -g -c condv-buffer.c -o condv-buffer.o gcc -D_REENTRANT -g -o condv-buffer condv-buffer.o -lpthread % ./condv-buffer
thread_A(): put( 0 ) thread_A(): put( 1 ) thread_A(): put( 2 ) thread_A(): put( 3 ) thread_B(): get() 0. thread_B(): get() 1. thread_B(): get() 2. thread_A(): put( 4 ) thread_A(): put( 5 ) thread_A(): put( 6 ) thread_B(): get() 3. thread_B(): get() 4. thread_B(): get() 5. thread_A(): put( 7 ) thread_A(): put( 8 ) thread_A(): put( 9 ) thread_B(): get() 6. thread_B(): get() 7. thread_B(): get() 8. thread_B(): get() 9. %
----------------------------------------------------------------------
バッファに要素を1つずつ追加しているので、
pthread_cond_signal() でもよい。
pthread_cond_broadcast() に変えても動くようにプログラムを
作る。
pthread_cond_wait() で待っている間に条件が変わっているかもしれないので、 最初から調べ直す。signal で1人だけしか起き上がらないと仮定してはいけ ない。
複数個同時に読み書きする時には、
pthread_cond_broadcast() でないとだめ。
迷った時には、pthread_cond_broadcast()。
<−>再帰呼出し
スレッド間でポインタを渡す時には、スレッドの寿命にも注意。
シングルスレッドのプログラムでは、static変数は、プログラムのモジュール性 を高めるために有効に使われてきた。
マルチスレッドと相性が非常に悪い。static変数もextern変数と同様に複数の スレッドで共有される。変更する場合には、mutex でロックが必要になる。
TCP/IP でプログラムを書く時に使うgethostbyname() は、
static変数に値をセットして、リターン・バリューとして返してる。
----------------------------------------------------------------------
struct hostent *gethostbyname( char *name ){
static struct hostent ret ;
.....
return( &ret );
}
----------------------------------------------------------------------
複数のスレッドが同時にこの関数を呼び出した場合、同じstatic変数が使われ
る。
複数のスレッドで呼び出してもきちんと動作することを、
スレッド・セーフ(thread-safe)
という。
MT-Safe(multi-thread-safe)
や
再入可能(reentrant)
ということもある。
externやstaticを使わず、auto変数やmalloc()だけを使っているような手続き は、スレッド・セーフ。
別のスレッド・セーフでない手続きを呼んでいれば、それはスレッド・セーフ ではない。
スレッド・セーフになるようにするには、インタフェースを変更する必要があ る。
Sun のマニュアルより:
----------------------------------------------------------------------
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyname_r(const char *name,
struct hostent *result, char *buffer, int buflen,
int *h_errnop);
----------------------------------------------------------------------
O2には、gethostbyname_r() はない。
-lrt オプションを付ける。
次のような関数が利用可能である。
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value)
int sem_wait(sem_t * sem)
int sem_trywait(sem_t * sem)
sem_wait()。0でも止まらずエラーを返す。
int sem_post(sem_t * sem)
int sem_getvalue(sem_t * sem, int * sval)
int sem_destroy(sem_t * sem)
注意:名前付きのセマフォもある。sem_open() で作成/初期化し、 sem_unlink() で削除する。
注意:Solaris には、POSIX のセマフォとは別に、カーネル内でのデバイス・ ドライバ作成のためのセマフォが用意されている。
ヒント: rp==wp の時に、バッファが空かフルかが判定ができればよい。
こういう改造にどのくらい意味があるか。
循環バッファのプログラムで、条件変数を1つになるように変更しなさい。一度に wait する必要があるのは、put() 側か get() のどちらか一方だけ。 あるいは、両方とも同じキューにつないでも、効率は悪いが動く事は動く。
こういう改造にどのくらい意味があるか。
レポートは、次のような形式の電子メールで送ること。
---------------------------------------------------------------------- To: yas@is.tsukuba.ac.jp Subject: [pdsoft/report-thread-1] <内容に関したサブジェクト> 学籍番号 000000 (各自の学籍番号で置き換える) 名前 漢字の名前 <本文> ---------------------------------------------------------------------- <内容>
注意:sakura の新城のログイン名は、yshinjo である。 レポートは、yas@is.tsukuba.ac.jp に送ること。 Subject: には、pdsoft という文字とレポートの番号(report-thread-1)を必 ず含めなさい。
電子メールの本文の先頭に学籍番号と名前(漢字の名前がある人は、漢字で) を書きなさい。
内容は、文法的に正しい、日本語、または英語で書くこと。文章には、述語を 付ける。体言止めは、使ってはいけない。単にプログラムを含めるのではなく、 「以下に○○のプログラムを示す」と書く。実行結果を必ず付ける。
送られたレポートには、確認応答(acknowledge)を返す。メッセージの紛失、 クラッシュ等に対応するために、時間切れによる再送などのリカバリは適宜や ること。
送ったレポートは、講義が終るまで保存しなさい。
循環バッファのプログラムを、セマフォを使って書き直しなさい。 酸素原子と水素原子から水が作られる反応を、スレッドを使って書きなさい。
水素原子や酸素原子ではなく、水素分子 (H2)や酸素分子 (O2)を使ってもよい。
1度に1方向の車しか通さない狭い橋がある。橋の上に同時に3台の車が通る と橋が落ちる。この橋が落ちないように、車の交通整理を実現するプログラム を書きなさい。車は、スレッドで実現されるものとする。そして、次のような 手続きを呼び出す。
vehicle(int direction) { arrive_bridge( direction ); cross_bridge( direction ); exit_bridge( direction ); }このコードでdirectionは、0 または 1 であり、橋のどの方向に車 が渡ろうとしているかを示している。 手続きarrive_bridge()とexit_bridge()を、mutex と条件 変数を使って書きなさい。arrive_bridge()は、安全にその方向で車 が通れるまでリターンしてはならない。衝突したり、重量オーバーで橋が落ち たりすることがないようにしなければならない。デバッグのためのメッセージ を適宜画面に出力する。exit_bridge()は、橋を渡り終えことを告げ るために呼ばれる。この時、可能ならば、待っている車を渡らせ始める (arrive_bridge()からリターンさせる)。ここでは、公平性は実現しなくてもよい。また、飢餓状態にならないことを保 証しなくてもよい。
完成したプログラムにおいて、新しい車が来た時、既に別の車が待っていたと する。この場合、新しい車が先に橋を渡るか、それとも古い車が先に橋を渡る か、それとも、予想できないか(非決定的か)。その理由を簡単に説明しなさ い。
↑[もどる] [1月18日] [1月25日] [2月1日]
Last updated: 2001/01/24 17:14:38
Yasushi Shinjo / <yas@is.tsukuba.ac.jp>