std.concurrency 応用編
std.concurrency
を使うことでメッセージパッシングスタイルの平行処理が出来るようになることは11日目の @repeatedly さんの記事によって示されていますが、今回はその応用。なので11日目を読んでいなければ先にそちらを読むことをおすすめします。
今回はstd.concurrency
の応用として、spawn
の引数に渡せるものや、send
の引数に渡せるもの==receive
/receiveOnly
で受け取れるものに関して、詳しく語っていきます。
今回の記事は、以下の進行に従って説明します。
- スレッドを超えて安全に取り扱われる型
-
spawn
の引数に指定することのできるもの -
send
/receive
できるもの
スレッドを超えて安全に取り扱われる型
スレッドを超えて安全に取り扱われる型とは、つまり、
!hasUnsharedAliasing!T
な、 T
のことです。
dlang.org:hasUnsharedAliasing
template hasUnsharedAliasing(T...)
各Tが以下のいずれかの条件をみたす場合にtrueを返します:
- 生のポインタU*を含み、Uはimmutableでもsharedでもない。
- 配列U[]を含み、Uはimmutableでもsharedでもない。
- classの参照型Cを含み、Cはimmutableでもsharedでもない。(訳注: たぶんinterfaceもこれと同じ)
- 連想配列型を含み、それがimmutableでもsharedでもない。
- delegateを含み、それがsharedではない。(訳注:たぶんimmutableが抜けてる)
(原文):
Returns true if and only if T's representation includes at least one of the following:
- a raw pointer U* and U is not immutable or shared;
- an array U[] and U is not immutable or shared;
- a reference to a class type C and C is not immutable or shared.
- an associative array that is not immutable or shared.
- a delegate that is not shared.
実はこのテンプレートはまさにこの目的のため、すなわち、ある型Tによって形成されるデータが、スレッドのコンテキスト間を渡って取り扱うことができるかどうかを判断することを主な目的としたテンプレートです。
裏返せば !hasUnsharedAliasing!T
であるということは、Tという型に拠るデータからたどることのできるすべての参照・ポインタ・デリゲートが shared
/immutable
/const shared
であること、要するにスレッドを超えて安全に取り扱われる型であることを保証します。
具体的に !hasUnsharedAliasing!T
を満たすのはどのような型か、ほんの一部ですが以下の例を御覧ください。
unittest
{
struct S2 { string a; }
struct S3 { int a; immutable Object b; }
static assert(!hasUnsharedAliasing!S2);
static assert(!hasUnsharedAliasing!S3);
struct S4 { int a; shared Object b; }
struct S6 { shared char[] b; }
struct S7 { float[3] vals; }
static assert(!hasUnsharedAliasing!S4);
static assert(!hasUnsharedAliasing!S6);
static assert(!hasUnsharedAliasing!S7);
struct S8 { int a; Rebindable!(immutable Object) b; }
static assert(!hasUnsharedAliasing!S8);
static assert(!hasUnsharedAliasing!(void delegate() immutable));
static assert(!hasUnsharedAliasing!(void delegate() shared));
static assert(!hasUnsharedAliasing!(void delegate() shared const));
static assert(!hasUnsharedAliasing!(const(void delegate() immutable)));
static assert(!hasUnsharedAliasing!(immutable(void delegate())));
static assert(!hasUnsharedAliasing!(shared(void delegate())));
static assert(!hasUnsharedAliasing!(shared(const(void delegate()))));
static assert(!hasUnsharedAliasing!(void function()));
static assert(!hasUnsharedAliasing!(Rebindable!(immutable Object)));
static assert(!hasUnsharedAliasing!(Rebindable!(shared Object)));
static assert(!hasUnsharedAliasing!(int, shared(int)*));
static assert(!hasUnsharedAliasing!(shared(int)*, Rebindable!(shared Object)));
static assert(!hasUnsharedAliasing!());
class S13
{
void delegate() shared a;
void delegate() immutable b;
shared(const(void delegate() shared const)) o;
}
static assert(!hasUnsharedAliasing!(immutable(S13)));
static assert(!hasUnsharedAliasing!(shared(S13)));
// などなど
}
ここでは!hasUnsharedAliasing!T
なものに限りましたが、Phobosのソースコードを読むとhasUnsharedAliasing!T
な例も見ることができます。
spawn
の引数に指定することのできるもの
spawn
関数によって新しいスレッドのコンテキストとなれるものは、以下の条件をすべて満たすものとなります。
-
spawn
の第一引数のF
はisCallable!F
。呼び出し可能な型であること。以下のいずれかを満たす。- function(関数ポインタ)
- delegate (デリゲート)
- opCallを実装したクラスオブジェクト/構造体(関数オブジェクト)
- 戻り値が
void
-
spawn
に指定した第二引数以降の引数T...
はすべて、暗黙にFの引数に指定可能 - Fは関数ポインタか、
!hasUnsharedAliasing!F
- 引数
T...
は!hasUnsharedAliasing!T
またはTid。スレッドを超えて安全に取り扱われる型であること。以下のいずれかを満たす。-
immutable
/shared
なターゲットを指定するポインタ -
immutable
/shared
な要素を持つ配列 -
immutable
/shared
なclass
/interface
-
immutable
/shared
な連想配列 -
immutable
/shared
なdelegate
- 参照を含まないデータ
Tid
-
ここでも出てくるのが !hasUnsharedAliasing!F
です。要するに、スレッドを超えて安全に取り扱われる関数呼び出し可能な型、例えばvoid delegate() shared
みたいな型である必要があります。
void delegate() shared
の作り方の詳細については 10日目 とか御覧ください。
さらに、コンテキストに指定するF
の引数T...
についても、!hasUnsharedAliasing!T
でなければならないという条件があります。
要するに、コンテキストはspawn
を呼び出したスレッドとは異なるスレッドで実行されるのだから、コンテキストそのものも、コンテキストに指定した関数の引数も、異なるスレッドで扱って問題ないデータでなければなりませんよ、ということです。
以下、例をご覧ください。
例1 11日目 @repeatedly さんの記事より抜粋
import std.stdio, std.concurrency;
void func(string message)
{
writeln(message);
}
void main()
{
spawn(&func, "Hello!");
}
上記コードでは、spawn
に関数ポインタを指定しており第1と第4の条件を満たします。また、funcの戻り値はvoidであり、第2の条件を満たします。さらに、引数の型はfunc
の仮引数、spawn
の第2実引数ともにstringで一致しており、第3の条件も問題なく満たしています。なお、引数の型string
は、immutable(char)[]
と等価、つまりhasUnsharedAliasing
の第2条件に一致しないため、 !hasUnsharedAliasing!(string) == true
となり、第5条件を満たします。
以下の表にまとめました。
条件番号 | 条件 | 判定対象 | 可否 |
---|---|---|---|
1 |
isCallable!F を満たす |
&func | ○ function |
2 | 戻り値がvoid
|
funcの戻り値型 | ○ void |
3 |
T... はF の引数に暗黙変換可能 |
stringとstring | ○ 完全一致 |
4 | Fは関数ポインタか!hasUnsharedAliasing!F
|
&func | ○ function |
5 |
!hasUnsharedAliasing!T を満たすかTid |
string | ○immutable な要素を持つ配列 |
例2
import std.stdio, std.concurrency;
class Callable
{
int a;
this(int x) shared
{
a = x;
}
void opCall(shared Callable callable, const shared int* x) shared
{
writefln("this.a=%s, callable.a=%s, arg x=%s", a, callable.a, *x);
}
}
class DerivedCallable: Callable
{
this(int x) shared
{
super(x*x);
}
}
void main()
{
auto callable1 = new shared Callable(1);
auto callable2 = new shared DerivedCallable(2);
shared int* a = new shared int;
*a = 100;
spawn(callable1, callable2, a);
}
条件を満たせば、上記コードのようなことも可能。
各条件を満たす理由については以下の表にまとめました。
条件番号 | 条件 | 判定対象 | 可否 |
---|---|---|---|
1 |
isCallable!F を満たす |
callable1 | ○ opCallを実装したクラスオブジェクト |
2 | 戻り値がvoid
|
Callable.opCall | ○ void |
3-1 |
T... はF の引数に暗黙変換可能 |
shared Callableとshared Callable | ○ 完全一致 |
3-2 |
T... はF の引数に暗黙変換可能 |
shared Callableとshared DerivedCallable | ○ 暗黙変換(アップキャスト)可能 |
3-3 |
T... はF の引数に暗黙変換可能 |
const shared int* とshared int* | ○ 暗黙変換(shared型からconst shared型への変換)可能 |
4 | Fは関数ポインタか!hasUnsharedAliasing!F
|
callable1 | ○ shared なclass なので!hasUnsharedAliasing!F を満たす |
5-1 |
!hasUnsharedAliasing!T を満たすかTid |
shared Callable | ○ shared なclass なので!hasUnsharedAliasing!T を満たす |
5-2 |
!hasUnsharedAliasing!T を満たすかTid |
shared Callable | ○ shared なclass なので!hasUnsharedAliasing!T を満たす |
5-3 |
!hasUnsharedAliasing!T を満たすかTid |
const shared int* | ○ shared なターゲットを指定するポインタなので!hasUnsharedAliasing!T を満たす |
send
/receive
できるもの
spawn
の引数が !hasUnsharedAliasing!T
でなければならないのと同様に、コンテキストが異なるスレッドで実行されている以上、そこに送付するデータについても、!hasUnsharedAliasing!T
でなければなりません。
ここまでならspawn
と同じなので簡単なのですが、実はもう一つ大きめの制限があります。
それは、実引数に指定するデータが、creal・文字列(char[])・デリゲートのうちビット長が最大の型のビット長を超えてはならない、というものです。
要するに、基本型、配列、クラスオブジェクト、デリゲート、ポインタくらいしか指定できず、構造体変数はたかだかポインタと同等サイズ程度のものしか指定できないと思ったほうがいいです。
この条件は、send
/receive
の内部で、Variant
型が使用されていることに由来します。Variant
に詰め込めるデータの条件が、まさに上記で述べた内容そのままなのです。
send
に関して、受け渡しの際にVariant
が使用される関係上、Variant
が許容するサイズ以下のデータしか引き渡すことができません。
(正直これはA先生も意図していない挙動な気がしますが、現状そのように実装されているのでそれが仕様なのでしょう…。)
(私はVariant
に制限がある事自体なんかダメなんじゃないかと思います)
もしも受渡したいデータのサイズが大い場合は、newしたうえで、ポインタによる受け渡しを行うと良いでしょう。
なお、ポインタや配列などを受け渡す際には、型名は正確に記述する必要があります。特に型を修飾する場合は要注意です。
receive((shared int* i) {} );
などという書き方はNGです。
receive((shared(int)* i) {} );
上記のように、型名を正確に記述します。
以下、例を示します。
例3 11日目 @repeatedly さんの記事より抜粋
import std.stdio, std.concurrency;
void func()
{
bool running = true;
while (running) {
receive((int i) { writeln("Received an int: ", i); },
(float f) { writeln("Received a float: ", f); },
(Variant v) {
writeln("Received unknown type. Terminated...");
running = false;
});
}
}
void main()
{
auto tid = spawn(&func);
send(tid, 42);
send(tid, 42.0f);
send(tid, "Hello!");
}
1つ目のsend
は42
つまり、int
型です。int
型は基本型で参照型を含まないので!hasUnsharedAliasing!T
を満たし、かつVariant
に指定可能なので条件を満たします。
2つ目のsend
は42.0f
つまり、float
型です。float
型は基本型で参照型を含まないので!hasUnsharedAliasing!T
を満たし、かつVariant
に指定可能なので条件を満たします。
3つ目のsend
は"Hello!"
つまり、immutable(char)[]
型です。配列ですが、要素がimmutable
なので!hasUnsharedAliasing!T
を満たし、かつVariant
に指定可能なので条件を満たします。
##例4
import std.stdio, std.concurrency;
// クラス
class A { }
// 大きいサイズの構造体
struct S { int[32] dat; }
void func()
{
bool running = true;
while (running) {
receive(// 以下2つの関数は、どちらが呼ばれるか確認して下さい。
(shared int* i) { writeln("Received an int ptr 1:", *i); },
(shared(int)* i) { writeln("Received an int ptr 2:", *i); },
// 以下のaは排他制御なしに無理やりキャストしていますが、これは
// main関数内で変更されないことに確証を得ているから行える行為です。
// 確証なしに排他制御なしでアクセスするのはNGです。
(shared A a) { writeln("Received a class object A:", (cast()a).toString()); },
// 大きいサイズの構造体は以下のようにポインタで受け渡しを行うのが吉。
(shared(S)* s) { writeln("Received a struct S: ", s.dat[]); },
(Variant v) {
writeln("Received unknown type. Terminated...");
running = false;
});
}
}
void main()
{
auto tid = spawn(&func);
auto i = new shared int;
auto a = new shared A;
auto s = new shared S;
*i = 123;
s.dat[] = 456;
// 以下、i, a, s の型がそれぞれ何かを確認して下さい。
pragma(msg, typeof(i));
pragma(msg, typeof(a));
pragma(msg, typeof(s));
send(tid, i);
send(tid, a);
send(tid, s);
send(tid, "Hello!");
}
まとめ
std.concurrency
の導入について、各関数の引数に何を指定すればよいのかについてお話しました。
- スレッドを超えて安全に取り扱われる型とは、
!hasUnsharedAliasing!T
を満たすT
のことで、shared
/immutable
のついていない参照データを含みません。 -
spawn
の引数に指定することのできるものとは、!hasUnsharedAliasing!F
を満たすF
と、F
の引数に暗黙変換可能な!hasUnsharedAliasing!T
を満たすT...
のことです。F
には関数ポインタ以外にも、void delegate() shared
などが含まれます。 -
send
/receive
できるものとは、!hasUnsharedAliasing!T
を満たし、かつVariant
に指定可能なT
のことです。また、receive
の際には型名を正確に記述しましょう。
この記事を読むとわかると思いますが、並行プログラミングを行う上で、shared
属性は!hasUnsharedAliasing!T
を満たす条件にとって、immutable
と同じくらい非常に高い重要性を持ちます。しかしながら、shared
の使いこなしの技術についてはまだまだ未発達な分野です。これらをうまく使おうと思うと、必ずshared
の使い方について幾つもの壁が出てくることでしょう。もし仮に、この記事を読み、shared
を使ってみて、使い方についていい方法や定石などが見えてくるようでしたら、是非記事にしてください。shared
を使う人と情報が増えることを心から願っております。
なお、私の書いたshared
の使い方についての記事は以下です。ぜひご参考にしてください。
sharedの話
shraedのcastについて