web-dev-qa-db-ja.com

foreachループでタスクを開始すると最後の項目の値が使用される

私は新しいタスクで初めて遊んでみましたが、理解できないことが起こりました。

まず、コードは非常に単純です。私はいくつかの画像ファイルへのパスのリストを渡し、それらのそれぞれを処理するタスクを追加しようとします:

public Boolean AddPictures(IList<string> paths)
{
    Boolean result = (paths.Count > 0);
    List<Task> tasks = new List<Task>(paths.Count);

    foreach (string path in paths)
    {
        var task = Task.Factory.StartNew(() =>
            {
                Boolean taskResult = ProcessPicture(path);
                return taskResult;
            });
        task.ContinueWith(t => result &= t.Result);
        tasks.Add(task);
    }

    Task.WaitAll(tasks.ToArray());

    return result;
}

たとえば、単体テストで3つのパスのリストを使用してこれを実行すると、3つのタスクすべてが、提供されたリストの最後のパスを使用することがわかりました。ステップスルーする場合(およびループの処理を遅くする場合)、ループからの各パスが使用されます。

誰かが何が起こっているのか、そしてその理由を説明できますか?可能な回避策?

47
Wonko the Sane

ループ変数を閉じています。それをしないでください。代わりにコピーを取ってください:

foreach (string path in paths)
{
    string pathCopy = path;
    var task = Task.Factory.StartNew(() =>
        {
            Boolean taskResult = ProcessPicture(pathCopy);
            return taskResult;
        });
    // See note at end of post
    task.ContinueWith(t => result &= t.Result);
    tasks.Add(task);
}

現在のコードはpathをキャプチャしています-タスクを作成するときのvalueではなく、変数自体です。その変数は、ループを通過するたびに値を変更します。そのため、デリゲートが呼び出されるまでに簡単に変更できます。

変数のコピーを取ることで、ループを通過するたびにnew変数を導入します-をキャプチャするとき変数。ループの次の反復では変更されません。

Eric Lippertには、これについて詳しく説明する2組のブログ投稿があります。 part 1 ; パート2

気分を悪くしないでください-これはほとんどすべての人を捕まえます:(


この行に関する注意:

task.ContinueWith(t => result &= t.Result);

コメントで指摘されているように、これはスレッドセーフではありません。複数のスレッドが同時にそれを実行する可能性があり、互いの結果にスタンプを押す可能性があります。質問が関心を寄せている主要な問題、つまり変数のキャプチャを混乱させるので、ロックや同様のものは追加していません。ただし、注意する価値があります。

83
Jon Skeet

StartNewに渡しているラムダはpath変数を参照しており、変数は反復ごとに変化します(つまり、ラムダは値だけではなくpathreferenceを使用しています)。変更するバージョンを指定しないように、そのローカルコピーを作成できます。

foreach (string path in paths)
{
    var lambdaPath = path;
    var task = Task.Factory.StartNew(() =>
        {
            Boolean taskResult = ProcessPicture(lambdaPath);
            return taskResult;
        });
    task.ContinueWith(t => result &= t.Result);
    tasks.Add(task);
}
12
bdukes