#どー言うことか
static void Main(string[] args)
{
for (int i = 0; i < 10; i++)
{
Task.Run(()=> Console.WriteLine(i));
}
Console.ReadLine();
}
こんな風に書いたら
6
10
5
8
5
10
8
8
6
5
こんな結果になった。
#なぜこんなことが起きたのか
一見すれば、上記のコードはTask.Run
メソッドは同期的に実行されるi
も、問題なくインクリメントしていくように見えるが、実際は予想できない結果になった。
これは、Task.Run
メソッド内でi
をキャプチャしてるので、クロージャになっている。そのため、以下のように展開されてしまっているのだ。
using System;
using System.Threading.Tasks;
namespace ConsoleApp1
{
public class Program
{
private class Anonymous
{
public int i;
public void Proc()
{
Console.WriteLine(i);
}
}
static void Main(string[] args)
{
var anony = new Anonymous();
for (int i = 0; i < 10; i++)
{
anony.i = i;
Task.Run(anony.Proc);
}
Console.ReadLine();
}
}
}
このように、匿名クラスのフィールドへいったん1度代入して、その後にそのフィールドをコンソールへ出力する形となる。
なので、従って、anony.i
の更新と、Task内でのConsole.WriteLine
の実行が制御されることなく行われるので、意図しない予見不可能な結果となる。
#わーくあらうんど(追記)
何も難しいこと考えずに、@munielさんや、@acpleさんから頂いたご指摘の通り、
下記のように、ループブロック内で一時変数に代入すれば解決が出来る。
using System;
using System.Threading.Tasks;
namespace ConsoleApp1
{
public class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 10; i++)
{
var tmp = i;
Task.Run(() => Console.WriteLine(tmp));
}
Console.ReadLine();
}
}
}
##これが問題なく動く理由
なぜ一時変数に代入することで、問題なく動くようになったのかはデコンパイル結果を見ればわかる。
下記は、デコンパイルした結果を再構成したソース。
using System;
using System.Threading.Tasks;
namespace ConsoleApp1
{
public class Program
{
class Anonymous
{
public int tmp;
public void Proc()
{
Console.WriteLine(tmp);
}
}
static void Main(string[] args)
{
for (int i = 0; i < 10; i++)
{
var anonymous=new Anonymous();
anonymous.tmp = i;
Task.Run(anonymous.Proc);
}
Console.ReadLine();
}
}
}
最初と異なり、匿名クラスのインスタンスが、ループ毎に作成されそのフィールドに値が代入されてProc
メソッドが呼ばれる。
従って、Procが見ているtmp
フィールドは実行されるタスク毎に独立する形となり、齟齬無く実行されることになる。
但し、匿名クラスがループ毎に作成されると言うことはループの数が大きい場合、GCが走りまくってパフォーマンスが悪化する恐れがあることは
意識しておかないとマズいかも知れない。
##まとめ
λの中でクロージャを使用した場合、大体は意図通りに動くけど、時として上記のようにぱっと見、意図通り動くはずのものが動かないことがある。
なので、クロージャを試用したときはその変数はローカル変数とは別の性質を持つと言うことを意識する必要があると思う。