Запуск задач в foreach Loop использует значение последнего элемента

Я делаю первую попытку играть с новыми Задачами, но что-то происходит, что я не понимаю.

Во-первых, код, который довольно прямолинейный. Я передаю список путей к некоторым файлам изображений и попытаюсь добавить задачу для обработки каждого из них:

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;
}

Я обнаружил, что если я просто позволю этому запустить, скажем, список из трех путей в unit test, все три задачи используют последний путь в указанном списке. Если я пройду (и замедляю обработку цикла), используется каждый путь из цикла.

Может кто-нибудь объяснить, что происходит, и почему? Возможные обходные пути?

Ответ 1

Вы закрываете переменную цикла. Не делай этого. Вместо этого сделайте копию:

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

Ваш текущий код захватывает path - не значение его при создании задачи, а сама переменная. Эта переменная меняет значение каждый раз, когда вы проходите цикл, поэтому он может легко измениться к моменту, когда вы вызываете делегата.

Приняв копию переменной, вы вводите новую переменную каждый раз, когда вы проходите цикл - когда вы фиксируете эту переменную, она не будет изменена в следующей итерации цикла.

У Эрика Липперта есть пара сообщений в блогах, которые идут в этом намного подробнее: часть 1; часть 2.

Не чувствую себя плохо - это улавливает почти всех: (

Ответ 2

Лямбда, передаваемая в StartNew, ссылается на переменную path, которая изменяется на каждой итерации (т.е. ваша лямбда использует ссылку path, а не только ее значение). Вы можете создать локальную копию этого файла, чтобы не указывать на версию, которая изменится:

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);
}