foreach循环中的启动任务使用最后一项的值[重复]

Posted

技术标签:

【中文标题】foreach循环中的启动任务使用最后一项的值[重复]【英文标题】:Starting Tasks In foreach Loop Uses Value of Last Item [duplicate] 【发布时间】:2011-01-13 19:27:39 【问题描述】:

我正在第一次尝试使用新任务,但发生了一些我不明白的事情。

首先,代码非常简单。我传入一些图像文件的路径列表,并尝试添加一个任务来处理它们:

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 个路径的列表,所有三个任务都使用提供的列表中的最后一个路径。如果我单步执行(并减慢循环的处理速度),则会使用循环中的每条路径。

谁能解释一下发生了什么,为什么?可能的解决方法?

【问题讨论】:

我可以建议使用 ReSharper。为您突出显示此特定错误和其他潜在错误 【参考方案1】:

您正在关闭循环变量。不要那样做。改为复印:

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 - 不是您创建任务时它的,而是变量本身。每次您通过循环时,该变量都会更改值 - 因此它可以在您的委托被调用时轻松更改。

通过获取变量的副本,您每次通过循环时都会引入一个 new 变量 - 当您捕获 那个 变量时,它不会在循环的下一次迭代中更改。

Eric Lippert 有两篇博客文章对此进行了更详细的介绍:part 1; part 2.

不要难过 - 这几乎让每个人都感到沮丧:(


注意这一行:

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

正如 cmets 中所指出的,这不是线程安全的。多个线程可以同时执行它,可能会影响彼此的结果。我没有添加锁定或任何类似的东西,因为它会分散问题感兴趣的主要问题,即变量捕获。但是,值得留意。

【讨论】:

当然。为树木而森林等等。 :) 这个闭包问题和 Random() 的不当使用必须在 SO 频率上排在前 5 位 这种行为已经改变,不仅在 C#5.0 中(如 Eric Lipperts 博客文章的更新中所述),如果你的目标是 4.0,在 VS2012 中也是如此。 @Snixtor:那仍然是 C# 5 编译器。区分您使用的 language 版本和您定位的 framework 版本非常重要。 此代码有一个竞争条件,即您将来自多个线程的结果聚合到 result 变量中,而没有正确同步。【参考方案2】:

您传递给 StartNew 的 lambda 引用了 path 变量,该变量在每次迭代中都会发生变化(即您的 lambda 正在使用 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);

【讨论】:

以上是关于foreach循环中的启动任务使用最后一项的值[重复]的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的委托只使用我的 foreach 循环中的最后一项?

foreach 循环重复数组中的最后一项

在 foreach 循环中使用 file_put_contents 只会下载最后一项

邦道科技面试题

Python“for in”循环打印列表中的最后一项

php中foreach使用引用的陷阱