概要
lock
ステートメントを利用した相互排他ロックを利用する際に留意すべき点をいくつかまとめてみました。
また、lock(<obj>)
の<obj>
の部分を以下ロックトークン(Lock token)
と呼称することにします。
型引数を利用した場合
メソッドなどのパラメータによって貰ってきたエンティティをロックトークンにすることはデッドロックをはじめとする厄介ゴトの根源みたいなもんなので避けるか使うにしても相当注意すべきでしょうが、まぁサンプルなので以下のようなメソッドが有るとします。
以下のようなメソッドを考えてみます。
static void LockSample<T>(T lockToken)
{
lock (lockToken)
{
foreach (var c in "Lock!")
{
Console.Write(c);
Thread.Sleep(10);
}
Console.WriteLine();
}
}
パラメータ使ったロックは色々と問題がありますが、まぁ話を単純化するってコトでご理解下さい。
こいつを、以下のように呼び出したとします。
static void Main(string[] args)
{
object lockToken=new object();
Parallel.For(0, 10, i => { LockSample(lockToken); });
}
この場合、想定通り、”Lock!”というキャラクタがコンソールに10個出て終了となります。
他方、下記の場合は想定外の結果になると思います。
static void Main(string[] args)
{
int lockToken = 10;
Parallel.For(0, 10, i => { LockSample(lockToken); });
}
実行結果は神のみぞ知るとしか言い様がありませんが、あらかた想定通りとは行かないと思ったり。
なんでこんなことになるのか?
本来、ロックトークンは参照型であることが条件なので、以下のようなコードはメソッド名通りCS0185
が発生しちまう。
static void OccurCS0185(int i)
{
lock (i)
{
foreach (var c in "Lock!")
{
Console.Write(c);
Thread.Sleep(10);
}
Console.WriteLine();
}
}
けれど、型引数を利用した場合、型引数にwhere T:class
のような制約無しで、コンパイルは通るし、実行も出来ちまう。
その結果、Tが値型になった場合、本来コンパイルすら出来ないのにコンパイルも実行も可能になってしまい、結果が破綻することになる。
ちなみに、なぜコンパイル出来ちまうかというと、先のLockSample
はばらしてみると概ね以下のような状況になってるから。
static void LockSampleReveal<T>(T lockToken)
{
object token = lockToken;
lock (token)
{
foreach (var c in "Lock!")
{
Console.Write(c);
Thread.Sleep(10);
}
Console.WriteLine();
}
}
一度、objecdt
型のローカル変数でlockToeken
を受けているので、参照型という条件を満たしている。但し、Tが値型の場合、token
への代入は、ボックス化が伴うので、呼び出し毎に別個のエンティティが作成されてしまいlockが用をなさなくなっちまう。
この辺、実はC#6のRCではコンパイルエラーになってくれていた。けれどリリースでRejectされちまった事実があるので、Microsoftも問題を認識しつつも、その影響のでかさゆえ手を着けることができないんじゃ無いかなとか思ってる。
まとめと蛇足
このように、型引数をロックトークンとしてしまった場合、意味を成さないロックになってしまうことが有るので、注意が必要となるかなと。
また、剥き身で使うことはそうそう無いだろうけど、Monitor.Enter
はロックトークンの型がobject
なので、基本何でも入るけど結果は先ほどと同じなので、その辺も注意した方が良いかなと思います。