...ing logging 4.0

はてなブログに移行しました。D言語の話とかいろいろ。

マルチスレッドプログラミングわかんねー(5)

勉強しながら書いているので鵜呑みにしたらダメですよ〜.

変数のスコープと生存期間

スコープと生存期間は違う.
関数内で宣言された自動変数のスコープと生存期間はその関数内だけ.
関数内で宣言されたstatic変数のスコープはその関数内だが,生存期間はプログラム実行中のすべて.
従って,static変数は(そのアドレスを)スコープ外に持っていっても正常にアクセスすることができる.
また,グローバル変数の生存期間もプログラム実行中のすべてだ.

スレッドローカルストレージ

マルチスレッドプログラミングでは,複数のスレッドを協調動作させるために,複数のスレッドからアクセスできる変数が必要になることが多い.
その変数は長い生存期間を持っていて欲しいのだが,このためにグローバル変数やstatic変数が使える.
一般的に,static変数とグローバル変数はグローバルなデータセグメントに配置され,複数のスレッドで共有される.
一方,専用のAPIや対応したプログラミング言語の機能を用いることによって,各スレッドで共有されず別の実体を持つ変数も作成することができる.
このような変数はスレッドローカルストレージ(TLS)やスレッド固有データと呼ばれる(スレッド局所記憶 - Wikipedia).

実践

D言語2.031ではstatic変数はデフォルトでTLSだ.
変数をsharedで修飾することで各スレッドで共有の変数を作ることができる.
(グローバルな)static変数を作って,一方をそのままにしてTLSに,もう一方をsharedで修飾して共有変数にしてみよう.


ここで大事なことは,マルチスレッドプログラミングの場合,別のスレッドから起動されるf()の中で見えている変数が,main()の中で見えている変数とは必ずしも実体が同じものではないかもしれないということだ.
実際,xはshared変数なので同じもので,yはTLS変数なので実体は異なるものだ.
main()のスレッドと,f()のスレッドの2つのスレッドがあるので,コード上には1つしか宣言されていないけれどもyの実体は2つあることになる.
それを踏まえて,実行結果を追ってみる.

import std.stdio;
import core.thread;
import std.c.windows.windows : Sleep;

shared static int x = 1;
static int y = 1;

void f()
{
	foreach(e; 0..5){
		writefln("\tB: shared_x=%d, y=%d", cast(int)x, y);//castはバグ回避のため
		x = 4;
		y = 4;
		Sleep(600);//600ms待つ
	}
	writeln("B: end");
}

void main()
{
	x = 2;
	y = 2;
	auto w = new Thread(&f);
	w.start();
	
	foreach(e; 0..5){
		writefln("A: shared_x=%d, y=%d", cast(int)x, y);
		x = 3;
		y = 3;
		Sleep(1000);//1000ms待つ
	}
	writeln("A: end");
}

これを実行すると次のようになった.なお,行番号は後で編集して追加している.

01| A: shared_x=2, y=2
02|         B: shared_x=2, y=1
03|         B: shared_x=4, y=4
04| A: shared_x=4, y=3
05|         B: shared_x=3, y=4
06|         B: shared_x=4, y=4
07| A: shared_x=4, y=3
08|         B: shared_x=3, y=4
09| B: end
10| A: shared_x=4, y=3
11| A: shared_x=3, y=3
12| A: end

まず,実行結果の1行目.
main()の中でxとyを2で初期化した後,w.start();でf()のスレッドを起動しているが,f()の方ではなくmain()のwritefln()の方が先に実行されて,x=2,y=2が表示された.


2行目では,f()の方でwritefln()が実行されるが,さっきy=2と表示されたはずなのにy=1になっている.
これはyがTLS変数だからだ.
f()の中から見るyの実体はmain()の中から見るyの実体とは異なる.


3行目では,f()のループが先に回り,xとyに4が代入されたことがわかる.


4行目では,今度はmain()のループが回り,x=4,y=3が表示された.
xはshared変数なので,f()の中から見るxとmain()の中から見るxの実体は同じであるため,f()の中での代入の影響を受けていることがわかる.


5行目は,xについて今起こったことと逆のことが起こっていて,main()での代入がf()から見るxに影響を与えている.


6行目は,3行目と同じ.
7行目は,4行目と同じ.
8行目は,5行目と同じだ.


9行目でf()のループが終了し,f()の実行が終了している.


10行目は,4行目および7行目と同じ.


11行目は,main()でxとyに3が代入されたことを示している.
12行目で,main()は終了.

今後の課題

f()をグローバルな関数でなくmain()内で作ったデリゲートにした場合どうなるのか.
また,関数内関数ではどうか.
これらを調べる予定.

今後の課題の結果

どっちでもグローバルな関数と一緒.
そりゃそうか.