私は新しいタスクで初めて遊んでみましたが、理解できないことが起こりました。
まず、コードは非常に単純です。私はいくつかの画像ファイルへのパスのリストを渡し、それらのそれぞれを処理するタスクを追加しようとします:
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つのタスクすべてが、提供されたリストの最後のパスを使用することがわかりました。ステップスルーする場合(およびループの処理を遅くする場合)、ループからの各パスが使用されます。
誰かが何が起こっているのか、そしてその理由を説明できますか?可能な回避策?
ループ変数を閉じています。それをしないでください。代わりにコピーを取ってください:
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);
コメントで指摘されているように、これはスレッドセーフではありません。複数のスレッドが同時にそれを実行する可能性があり、互いの結果にスタンプを押す可能性があります。質問が関心を寄せている主要な問題、つまり変数のキャプチャを混乱させるので、ロックや同様のものは追加していません。ただし、注意する価値があります。
StartNew
に渡しているラムダはpath
変数を参照しており、変数は反復ごとに変化します(つまり、ラムダは値だけではなくpath
のreferenceを使用しています)。変更するバージョンを指定しないように、そのローカルコピーを作成できます。
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);
}