並行システム
システム情報系情報工学域,
システム情報工学研究科コンピュータサイエンス専攻
新城 靖
<yas@cs.tsukuba.ac.jp>
このページは、次の URL にあります。
http://www.cs.tsukuba.ac.jp/~yas/cs/csys-2011/2011-12-15
あるいは、次のページから手繰っていくこともできます。
http://www.cs.tsukuba.ac.jp/~yas/cs/
http://www.cs.tsukuba.ac.jp/~yas/
目的
プロセスが、あるアクセスの仕方を守れば、メモリは「正しく」動作する ことを保証する。
制限が少ないと、遅くなる。使える範囲でいかに手を抜く(コストを下げる) かが大事になる。
以下で「メモリ」は、data store の意味で使う。変数かもしれないし、ファ イルかもしれないし、ページかもしれない。
メモリがページで、プロセスがプロセッサなら、分散共有メモリ(distributed shared memory,DSM)という。
a=1; a=2; print(a);
厳密一貫性では、常に 2 がプリントされる。
厳密一貫性では、「最新」の定義で、ニュートン物理学での絶対的な時間が必 要になる。
3 m 離れたコンピュータでは、光の速さででも 10ns かかる。 a=2; と print(a); 差が 1ns 以内という要求なら、光の 10 倍の速度が必要になる。
3 m
P1 <---------> P2
a=1;
a=2;
print(a);
P1、P2 はプロセス。
SMP(集中システム)ですら、キャッシュがあれば、実現不可能。
救い:実際の応用では、そこまで要求されていない。
例:厳密一貫性がある(横方向が時間軸、右に向って増加)
P1: W(x)1 P2: R(x)1例:厳密一貫性がない
P1: W(x)1 P2: R(x)0 R(x)1
いかなる実行の結果は、全てのプロセスによるメモリ(data store)に対する読 み書き操作が、ある逐次的な順序で行われた時と常に同じになる。ただし、個々 のプロセスによる操作は、メモリに対してそのプログラムに書かれた順序で行 われる。
インターリーブは、許されるが、インターリーブのされ方は、全てのプロセス から見て同じでなければならない。
例:厳密一貫性がないが、順序一貫性がある(横方向が時間軸、右に向って増加)
P1: W(x)1 P2: R(x)0 R(x)1例:厳密一貫性も、順序一貫性もある。
P1: W(x)1 P2: R(x)1 R(x)1
P1()
{
x=1 ;
print(y,z);
}
P2()
{
y=1 ;
print(x,z);
}
P3()
{
z=1 ;
print(x,y);
}
正しい実行系列の例(a),(b),(c),(d)(縦方向が、時間軸で、下に向って増加)。 正しい系列は、全部で90個ある。
Prints: は、画面に時系列で現れた表示、 Signature: は、プロセス P1, P2, P3 の出力をその順で繋げたもの。
(a)
x=1 ;
print(y,z);
y=1 ;
print(x,z);
z=1 ;
print(x,y);
Prints: 001011
Signature: 001011
(b)
x=1 ;
y=1 ;
print(x,z);
print(y,z);
z=1 ;
print(x,y);
Prints: 101011
Signature: 101011
(c)
y=1 ;
z=1 ;
print(x,y);
print(x,z);
x=1 ;
print(y,z);
Prints: 010111
Signature: 110101
(d)
y=1 ;
x=1 ;
z=1 ;
print(x,z);
print(y,z);
print(x,y);
Prints: 111111
Signature: 110101
考えられる Signature は、26==64 個あるが、全部は許されてい
ない。
P1: W(x)1 P2: R(x)0 R(x)1プロセスの履歴
H1 = W(x)1 H2 = R(x)0 R(x)1履歴から、2つの制約を満たすような、文字列 S を合成する。
制約1:プログラム順序が保たれる。
Hi = ... A .... B .... なら S = ..... A ..... B ....
制約2:メモリの整合性(memory coherence)が保たれる。 ある位置 x に対する読み取りは、常に最近の書込みの値を返す。
これらの制約を満たす S は、今の例では1つだけ。
S = R(x)0 W(x)1 R(x)1いくつかある正しい S のうち、1つを満たせば 順序一貫性を満たしている。
読み取りを速くすると、書込みが遅くなる。
順序一貫性を弱めたもの。
メーリング・リストで、元のメールと、それを引用したメールで、引用した方 が先に届いて目にすることがある。これは、因果一貫性が満たされていない。 こういう状況を無くしたい。
潜在的な因果関係がある: プロセス P1 が x に書く。 プロセス P2 が x を読んで y に書く。
並行性がある: 因果関係がないもの。
因果一貫性があるメモリ(data store):
潜在的に因果関係のある書込みは、全プロセスに同じ順序で見えなければならない。 並行性がある書込みは、異なる順序で見えてもよい。
因果一貫性は満たすが、順序一貫性(厳密一貫性も)を満たさない例
P1: W(x)1 W(x)3 P2: R(x)1 W(x)2 P3: R(x)1 R(x)3 R(x)2 P4: R(x)1 R(x)2 R(x)3P2 が、x を Read して Write している。 P1 の W(x)3 と P2の W(x)2 は、並行なので、見え方が違ってもよい。
因果一貫性を満たさない例。
P1: W(x)1 P2: R(x)1 W(x)2 P3: R(x)2 R(x)1 P4: R(x)1 R(x)2P2 が、x を Read して Write している。
因果一貫性を満す。
P1: W(x)1 P2: W(x)2 P3: R(x)2 R(x)1 P4: R(x)1 R(x)2P1 の W と P2 の W は、並行なので、見え方が違ってもよい。
命令と命令の依存関係を追跡しなければならない。
FIFO一貫性を満たす(因果一貫性を満たさない)例。
P1: W(x)1 P2: R(x)1 W(x)2 P3: R(x)2 R(x)1 P4: R(x)1 R(x)2
FIFO一貫性では、2つのプロセスが止まってしまうことがあり得る例。
int a=0;
int b=0;
P1()
{
a = 1 ;
if( b == 0 ) kill( P2 );
}
P2()
{
b = 1 ;
if( a == 0 ) kill( P1 );
}
弱一貫性の特性
2. は、パイプラインのフラッシュの意味。
3. があるので、同期をとってから普通の変数にアクセスすれば最新の状態に なっていることが言える。
弱一貫性を満たす
P1: W(x)1 W(x)2 S P2: R(x)1 R(x)2 S P3: R(x)2 R(x)1 SSは、同期変数のアクセス。
弱一貫性を満たさない
P1: W(x)1 W(x)2 S P2: S R(x)1
プロセスが Acquire した時、メモリは、必要ならそれらの共有変数の全コピー をリモートのコピーと一致させる。
リリース一貫性を満たす
P1: Acq(L) W(x)1 W(x)2 Rel(L) P2: Acq(L) R(x) 2 Rel(L) P3: R(x)1
分散システムでの実装は、2つの方法
T1: R(x)1 pthread_create(T2) T2開始: R(x)1pthread_create() 後に書き込んだものは、新しいスレッドが開始前でも読めな くてもよい。
T1: R(x)1 pthread_create(T2) W(x)2 T2開始: R(x)1
T1: R(x)1 unlock() T2: lock() R(x)1最初のスレッドが unlock 後にメモリが書かれた時、 別のスレッドが lock 後に読めなくても良い。
T1: unlock() W(x)1 T2: lock() R(x)0
T1: R(x)1 exit() T2: join() R(x)1終了後に書かれたものでも、join した時に見えなくてもよい。
T1: R(x)1 exit() T2: join() R(x)1 T3: W(x)2
T1: R(x)1 broadcast() T2: wait()から復帰 R(x)1起した後の書き込みは、見えなくてもよい。
T1: R(x)1 broadcast() W(x)2 T2: wait()から復帰 R(x)1
スレッドA スレッドB
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex1);
varabileA = 1;
varabileB = 2;
pthread_mutex_unlock(&mutex1);
localA = varabileA;
localB = varabileB;
pthread_mutex_unlock(&mutex1);
スレッドBは、スレッドAと同じ値を見る。
A=1, B=2。
スレッドA スレッドB
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex1);
varabileA = 1;
pthread_mutex_unlock(&mutex1);
varabileB = 2;
localA = varabileA;
localB = varabileB;
pthread_mutex_unlock(&mutex1);
スレッドBは、スレッドAと同じ値を見ないことがある。
スレッドBでは、A=1だが、Bは2ではないかもしれない。
スレッドA スレッドB
int sleeping=1;
while(sleeping)
continue;
sleeping=0;
スレッドBは、いつまでたってもループしているかもしれない。
コンパイラが無限ループのコードを出すことがある。
スレッドA スレッドB
volatile int sleeping=1;
while(sleeping)
continue;
sleeping=0;
コンパイラが無限ループのコードを出さない。
毎回、メモリから読む。
しかし、同期がないので、危ない。
A=1; B=2;メモリには、A, B の順に届くとは限らない。

図? プロセスのアドレス空間(単一スレッド)
機械語を書き換えながら実行するプログラムは、最近は行儀が悪いとされてい る。キャッシュの関係で特別な操作をしないと動かない。
malloc() で割り当てたヒープ上の変数は、
このセグメントに置かれる。
(
自動変数(auto)
は、次のスタック・セグメントに置かれる。
)
データセグメントは, 次の3つにわかれている。
malloc() で確保する変数が置かれるところ
static int x=100 ;
と書くと xはデータセグメントに割りつけられ,
実行形式ファイルに100という整数のビットパタンが含まれる。
BSSとは初期値を指定しない変数が置かれる場所である。 C言語で
static int x ;
と書くと yはBSSに割りつけられる。実行形式のファイルに
は、BSSの領域はない。これは実行時にカーネルが領域を割り当て、内容
を0に初期化する。
ヒープとは実行時に大きさが決まる変数を置くための領域である。 C言語で
char *p ;
int size ;
...
size = 100;
p = malloc( size );
とすると100バイトの領域がヒープ上に確保される。
ヒープ領域の大きさは、brk() システム・コールや
sbrk() システム・コールで変えることができる。これらの
システム・コールは、malloc() ライブラリ関数から呼び出さ
れる。
C言語で
main()
{
auto int z;
}
とすると、 4バイトの領域がスタックセグメントに割りつけられる。
(autoというキーワードは、普通省略される。スタックセグメ
ントは、普通、0xffffff (32ビットのアドレス空間の場合)からからアドレス
が小さくなるほうに向かって伸びていく。この部分は、関数呼び出しが続くと
伸びていき、割り当てられたメモリよりも多くなると、カーネルが自動的に拡
張する。
main()関数が3つの引数で呼び出される
ようになっている。
main( int argc, char *argv[], *envp[] )
{
}
ここで、
argc
が、引数の数(argvの数) ,
argv
が、引数のベクタの先頭番地、
envp
が環境変数のベクタの先頭番地である。この引数と環境変数は、スタックセグ
メントのスタックの底、
図?
では上(アドレスが大きいところ)にある。
1: /*
2: vaddr-print.c -- 変数の番地をしらべるプログラム
3: ~yas/syspro/proc/vaddr-print.c
4: Created on: 1997/05/19 22:58:49
5: */
6: #include <stdlib.h>
7: #include <stdio.h>
8:
9: #if defined(__APPLE__)
10: #include <mach-o/getsect.h>
11: #else
12: extern int etext, edata, end ;
13: #define get_end() (&end)
14: #define get_etext() (&etext)
15: #define get_edata() (&edata)
16: #endif
17:
18: void recursive( int n );
19:
20: int x1=1 ;
21: int x2 ;
22:
23:
24: main( int argc, char *argv[], char *envp[] )
25: {
26: auto int x3 ;
27: auto char *x4p ;
28:
29: printf("&main == %016p (text)\n",&main );
30: printf("&etext == %016p (text)\n",get_etext() );
31: printf("&edata == %016p (data)\n",get_edata() );
32: printf("&end == %016p (data)\n",get_end() );
33:
34: printf("&argv[0] == %016p (stack)\n",&argv[0] );
35: printf(" argv[0] == %016p (stack)\n", argv[0] );
36: printf("&envp[0] == %016p (stack)\n",&envp[0] );
37: printf(" envp[0] == %016p (stack)\n", envp[0] );
38:
39: printf("&x1 == %016p (data)\n",&x1 );
40: printf("&x2 == %016p (bss)\n",&x2 );
41: printf("&x3 == %016p (stack)\n",&x3 );
42: printf("&x4p == %016p (stack)\n",&x4p );
43: x4p = malloc( 10 );
44: printf("x4p == %016p (heap)\n",x4p );
45: x4p = malloc( 10 );
46: printf("x4p == %016p (heap)\n",x4p );
47: recursive( 3 );
48: }
49:
50: void recursive( int n )
51: {
52: int x5 ;
53: printf("&x5 == %016p (stack,%d)\n",&x5,n );
54: if( n<=0 )
55: return;
56: recursive( n-1 );
57: }
実行例。
% wget http://www.cs.tsukuba.ac.jp/~yas/cs/csys-2011/2011-12-15/ex/vaddr-print.c
% cc vaddr-print.c -o vaddr-print
% ./vaddr-print
&main == 0x0000000000275c (text)
&etext == 0x00000000002a94 (text)
&edata == 0x00000000003018 (data)
&end == 0x00000000006000 (data)
&argv[0] == 0x000000bffff67c (stack)
argv[0] == 0x000000bffff724 (stack)
&envp[0] == 0x000000bffff684 (stack)
envp[0] == 0x000000bffff732 (stack)
&x1 == 0x00000000003014 (data)
&x2 == 0x000000000030d4 (bss)
&x3 == 0x000000bffff598 (stack)
&x4p == 0x000000bffff59c (stack)
x4p == 0x00000000500130 (heap)
x4p == 0x00000000500140 (heap)
&x5 == 0x000000bffff538 (stack,3)
&x5 == 0x000000bffff4d8 (stack,2)
&x5 == 0x000000bffff478 (stack,1)
&x5 == 0x000000bffff418 (stack,0)
% size vaddr-print
__TEXT __DATA __OBJC others dec hex
8192 4096 0 12288 24576 6000
% otool -L vaddr-print
vaddr-print:
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 88.1.12)
% uname -mrsv
Darwin 8.11.0 Darwin Kernel Version 8.11.0: Wed Oct 10 18:26:00 PDT 2007; root:xnu-792.24.17~1/RELEASE_PPC Power Macintosh
%
% wget http://www.cs.tsukuba.ac.jp/~yas/cs/csys-2011/2011-12-15/ex/vaddr-print.c
% cc vaddr-print.c -o vaddr-print
% ./vaddr-print
&main == 0x000000004004d8 (text)
&etext == 0x00000000400746 (text)
&edata == 0x00000000600bb0 (data)
&end == 0x00000000600bc0 (data)
&argv[0] == 0x007fff320f5d08 (stack)
argv[0] == 0x007fff320f7998 (stack)
&envp[0] == 0x007fff320f5d18 (stack)
envp[0] == 0x007fff320f79a6 (stack)
&x1 == 0x00000000600bac (data)
&x2 == 0x00000000600bbc (bss)
&x3 == 0x007fff320f5c1c (stack)
&x4p == 0x007fff320f5c10 (stack)
x4p == 0x00000017c2b010 (heap)
x4p == 0x00000017c2b030 (heap)
&x5 == 0x007fff320f5bdc (stack,3)
&x5 == 0x007fff320f5bac (stack,2)
&x5 == 0x007fff320f5b7c (stack,1)
&x5 == 0x007fff320f5b4c (stack,0)
% size vaddr-print
text data bss dec hex filename
1955 504 16 2475 9ab vaddr-print
% ldd vaddr-print
libc.so.6 => /lib64/libc.so.6 (0x000000367e800000)
/lib64/ld-linux-x86-64.so.2 (0x000000367e400000)
%
int x;
f()
{
int y ;
static int z ;
}
| 種類 | スコープ | 寿命 |
|---|---|---|
| extern 外部変数 | 全プログラム | プログラムの実行開始から終了まで |
| auto 普通の局所変数 | 関数内 | その関数の実行中とそこから別の関数を呼んでいる間 |
| staticな局所変数 | 関数内 | プログラムの実行開始から終了まで |
| static変数(関数の外に定義) | 同一ファイル内 | プログラムの実行開始から終了まで |
| (C言語にはない) | (いろいろ) | プログラムの終了後も残る(永続的) |
auto変数のスコープは、関数内。
auto変数より広い範囲で使えるスレッドごとのデータが必要になる。
tsd[thread_id][key];
pthread_key_create()
pthread_setspecific()
pthread_getspecific()
1:
2: /*
3: * tsd-counter.c
4: * Created on: 2001/01/08 08:51:25
5: */
6:
7: #include <stdio.h> /* stderr */
8: #include <stdlib.h> /* malloc() */
9: #include <string.h> /* memset() */
10: #include <pthread.h>
11:
12: struct tsd_counter
13: {
14: int tc_value ;
15: };
16:
17: static pthread_key_t tsd_counter_tsdkey ;
18:
19: void
20: tsd_counter_module_init()
21: {
22: pthread_key_create( &tsd_counter_tsdkey,free );
23: }
24:
25: static struct tsd_counter *
26: tsd_counter_alloc()
27: {
28: struct tsd_counter *tc ;
29:
30: tc = malloc( sizeof(struct tsd_counter) );
31: if( tc == 0 )
32: {
33: fprintf(stderr,"no memory for struct tsd_counter\n");
34: exit( -1 );
35: }
36: memset( tc, 0, sizeof(struct tsd_counter) );
37: if( pthread_setspecific( tsd_counter_tsdkey, tc ) != 0 )
38: {
39: fprintf(stderr, "pthread_setspecific().\n");
40: exit( -1 );
41: }
42: return( tc );
43: }
44:
45: static struct tsd_counter *
46: tsd_counter_gettsd()
47: {
48: struct tsd_counter *tc ;
49: tc = pthread_getspecific( tsd_counter_tsdkey );
50: if( tc == 0 )
51: {
52: tc = tsd_counter_alloc();
53: }
54: return( tc );
55: }
56:
57: void tsd_counter_reset( int val )
58: {
59: struct tsd_counter *tc ;
60: tc = tsd_counter_gettsd();
61: tc->tc_value = val ;
62: }
63:
64: void tsd_counter_up()
65: {
66: struct tsd_counter *tc ;
67: tc = tsd_counter_gettsd();
68: tc->tc_value ++ ;
69: }
70:
71: int tsd_counter_getvalue()
72: {
73: struct tsd_counter *tc ;
74: tc = tsd_counter_gettsd();
75: return( tc->tc_value );
76: }
TSD 利用のパタン
pthread_getspecific() の呼出しを節約するため。
pthread_key_create() で初
期化する。確実に一度だけ初期化するために、main() から初期化の関数を呼び
出すか、pthread_once()を使う。
pthread_getspecific() でポインタを得る。
malloc() して、初期化して、
pthread_setspecific() で登録する。
pthread_key_delete() は、普通、使われない。ダイナミック・リンク
のモジュールを unload する時に必要になる(かもしれない)。
1:
2: /*
3: * tsd-counter-main.c by Y.Shinjo
4: * Created on: 2001/01/08 08:51:25
5: */
6:
7: #include <stdio.h> /* printf() */
8: #include <pthread.h>
9:
10: void thread1( int x );
11: void thread2( int x );
12:
13: extern void tsd_counter_module_init();
14: extern void tsd_counter_reset( int val );
15: extern void tsd_counter_up();
16: extern int tsd_counter_getvalue();
17:
18: main()
19: {
20: pthread_t t1 ;
21: pthread_t t2 ;
22: tsd_counter_module_init();
23: pthread_create( &t1, NULL, (void *)thread1, (void *)10 );
24: pthread_create( &t2, NULL, (void *)thread2, (void *)20 );
25: printf("main()\n");
26: pthread_join( t1, NULL );
27: pthread_join( t2, NULL );
28: }
29:
30: void thread1( int x )
31: {
32: int i ;
33: tsd_counter_reset( x );
34: printf("thread1(): value=%d \n", tsd_counter_getvalue() );
35: for( i = 0 ; i<3 ; i++ )
36: {
37: tsd_counter_up();
38: printf("thread1(): value=%d \n", tsd_counter_getvalue() );
39: }
40: }
41:
42: void thread2( int x )
43: {
44: int i ;
45: tsd_counter_reset( x );
46: printf("thread2(): value=%d \n", tsd_counter_getvalue() );
47: for( i = 0 ; i<3 ; i++ )
48: {
49: tsd_counter_up();
50: printf("thread2(): value=%d \n", tsd_counter_getvalue() );
51: }
52: }
実行例。
% wget http://www.cs.tsukuba.ac.jp/~yas/cs/csys-2011/2011-12-15/ex/tsd-counter.c
% wget http://www.cs.tsukuba.ac.jp/~yas/cs/csys-2011/2011-12-15/ex/tsd-counter-main.c
% wget http://www.cs.tsukuba.ac.jp/~yas/cs/csys-2011/2011-12-15/ex/Makefile
% emacs Makefile
% make
gcc -c -o tsd-counter-main.o tsd-counter-main.c
gcc -c -o tsd-counter.o tsd-counter.c
gcc -o tsd-counter-main tsd-counter-main.o tsd-counter.o -lpthread
% ./tsd-counter-main
thread1(): value=10
main()
thread2(): value=20
thread1(): value=11
thread2(): value=21
thread1(): value=12
thread2(): value=22
thread1(): value=13
thread2(): value=23
%
スレッド固有データの特徴
ただし意味が違うので、mutex と相互に書き換えできないことがある。 プロセス全体のデータを扱うものは、mutex で書くしかない。 Time Zoneなど。
スレッドごとに乱数生成器を持ってもいいのか。 再現性が必要か。mutex では、再現性はない。
実行時特化(部分評価)の技術を使うと、pthread_getspecific() の関数呼出 しが消えて、スレッド固有データが普通の extern や static 変数と同じにな る。
Yasushi Shinjo, Calton Pu. "Achieving Efficiency and Portability in Systems Software: A Case Study on POSIX-Compliant Multithreaded Programs," IEEE Transactions on Software Engineering, vol. 31, no. 9, pp. 785-800, September, 2005. http://doi.ieeecomputersociety.org/10.1109/TSE.2005.98
Figure 6: Runtime specialization in Tempo.
pthread_key_t tsdkey1 ;
iret_tsd() {
int *p ;
p = pthread_getspecific( tsdkey1 );
return( *p );
}
Figure 4: A function returning a value of a TSD variable.
int var1 ;
iret_extern() {
return( var1 );
}
Figure 5: A function returning a value of an external variable
entry_point := "iret_tsd()";
static_locations := ["tsdkey1"];
external_functions :=
EVALUATE["pthread_getspecific"];
lift_all := true ;
Figure 10: Hints in ML for specializing iret_tsd() .
void set_analysis_context(){
}
int var1 ;
void *pthread_getspecific(unsigned key) {
return( &var1 );
}
Figure 11: Hints in C for specializing iret_tsd() .
int H0;
int tmp_iret_tsd_1(){
return *((int *)(&H0));
}
Figure 12: The specialized code of iret_tsd() .
ライブラリを実現する場合、自分自身では Runnable を定義できないことがあ る。その時には、ThreadLocal クラスを使って、Pthread と似たようなことをする。
| Java | Pthread |
| final | pthread_once() |
| java.lang.ThreadLocal() | pthread_key_create() |
| get() | pthread_getspecific() |
| set() | pthread_setspecific() |
| initialValue() | - |
| (GC) | pthread_key_delete() |
int x=0;
int y=0;
int a=0;
int b=0;
threadA() {
a = 1;
x = b;
printf("threadA: %d,%d\n",a,x);
}
threadB() {
b = 1;
y = a;
printf("threadB: %d,%d\n",b,y);
}
このプログラムを実行した時に、どのような結果が画面に表示されるか。表示
される可能性のある例を、2つ以上示しなさい。