19

次の動作が発生しました。

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(() => {
        Debug.Print("Error: " + i.ToString());
    });
}

一連の「エラー:x」が発生します。ここで、xのほとんどは50に等しくなります。

同様に:

var a = "Before";
var task = new Task(() => Debug.Print("Using value: " + a));
a = "After";
task.Start();

結果は「値の使用:後」になります。

これは明らかに、ラムダ式の連結がすぐには発生しないことを意味します。式が宣言されたときに、ラムダ式で外部変数のコピーを使用するにはどうすればよいですか?以下はうまく機能しません(これは必ずしも一貫性がないわけではありません、私は認めます):

var a = "Before";
var task = new Task(() => {
    var a2 = a;
    Debug.Print("Using value: " + a2);
});
a = "After";
task.Start();
4

4 に答える 4

31

これは、スレッド化よりもラムダと関係があります。ラムダは、変数の値ではなく、変数への参照をキャプチャします。つまり、コードでiを使用しようとすると、その値は最後にiに格納されたものになります。

これを回避するには、ラムダの開始時に変数の値をローカル変数にコピーする必要があります。問題は、タスクの開始にオーバーヘッドがあり、ループが終了した後にのみ最初のコピーが実行される可能性があることです。次のコードも失敗します

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(() => {
        var i1=i;
        Debug.Print("Error: " + i1.ToString());
    });
}

James Manningが指摘したように、ループにローカル変数を追加して、そこにループ変数をコピーできます。このようにして、ループ変数の値を保持するために50の異なる変数を作成していますが、少なくとも期待どおりの結果が得られます。問題は、多くの追加の割り当てを取得することです。

for (var i = 0; i < 50; ++i) {
    var i1=i;
    Task.Factory.StartNew(() => {
        Debug.Print("Error: " + i1.ToString());
    });
}

最善の解決策は、ループパラメーターを状態パラメーターとして渡すことです。

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(o => {
        var i1=(int)o;
        Debug.Print("Error: " + i1.ToString());
    }, i);
}

状態パラメーターを使用すると、割り当てが少なくなります。逆コンパイルされたコードを見る:

  • 2番目のスニペットは50のクロージャと50のデリゲートを作成します
  • 3番目のスニペットは50個のボックス化されたintを作成しますが、デリゲートは1つだけです
于 2012-06-15T11:02:18.277 に答える
4

これは、新しいスレッドでコードを実行していて、メインスレッドがすぐに変数を変更するためです。ラムダ式がすぐに実行された場合、タスクを使用するポイント全体が失われます。

スレッドは、タスクの作成時に変数の独自のコピーを取得しません。すべてのタスクは同じ変数を使用します(実際には、メソッドのクロージャーに格納され、ローカル変数ではありません)。

于 2012-06-15T10:58:12.323 に答える
3

ラムダ式は、外部変数の値ではなく、その変数への参照をキャプチャします。50それがあなたがあなたの仕事を見たり、したりする理由ですAfter

これを解決するには、ラムダ式の前にそのコピーを作成して、値でキャプチャします。

この不幸な動作は、.NET 4.5を使用するC#コンパイラによって修正されます。それまでは、この奇妙な状況に耐える必要があります。

例:

    List<Action> acc = new List<Action>();
    for (int i = 0; i < 10; i++)
    {
        int tmp = i;
        acc.Add(() => { Console.WriteLine(tmp); });
    }

    acc.ForEach(x => x());
于 2012-06-15T11:00:51.700 に答える
2

ラムダ式は定義上遅延評価されるため、実際に呼び出されるまで評価されません。あなたの場合、タスクの実行によって。ラムダ式でローカルを閉じると、実行時のローカルの状態が反映されます。あなたが見るものはどれですか。これを利用できます。たとえば、forループは、説明された結果が意図したものであると仮定して、反復ごとに新しいラムダを実際に必要としません。

var i =0;
Action<int> action = () => Debug.Print("Error: " + i);
for(;i<50;+i){
    Task.Factory.StartNew(action);
}

一方、実際に印刷し"Error: 1"..."Error 50"たい場合は、上記を次のように変更できます。

var i =0;
Func<Action<int>> action = (x) => { return () => Debug.Print("Error: " + x);}
for(;i<50;+i){
    Task.Factory.StartNew(action(i));
}

最初のものは閉じて、アクションが実行されたときiの状態を使用しますi。状態は、ループが終了した後の状態になることがよくあります。後者の場合i、関数の引数として渡されるため、熱心に評価されます。次に、この関数Action<int>はに渡されるを返しますStartNew

したがって、設計上の決定により、遅延評価と熱心な評価の両方が可能になります。怠惰な理由は、ローカルが閉じられているため、そしてローカルを引数として渡すか、以下に示すように、より短いスコープで別のローカルを宣言することによって、ローカルを強制的に実行させることができるためです。

for (var i = 0; i < 50; ++i) {
    var j = i;
    Task.Factory.StartNew(() => Debug.Print("Error: " + j));
}

上記はすべてLambdasの一般的なものです。の特定のケースでは、StartNew実際には2番目の例と同じように過負荷が発生するため、次のように簡略化できます。

var i =0;
Action<object> action = (x) => Debug.Print("Error: " + x);}
for(;i<50;+i){
    Task.Factory.StartNew(action,i);
}
于 2012-06-15T11:06:25.290 に答える