在foreach循环中启动任务使用最后一项的值

Won*_*ane 46 c# multithreading task-parallel-library

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

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

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;
}
Run Code Online (Sandbox Code Playgroud)

我发现,如果我让它运行,例如,单元测试中的3个路径列表,则所有三个任务都使用提供列表中的最后一个路径.如果我单步执行(并减慢循环的处理速度),则使用循环中的每个路径.

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

Jon*_*eet 87

你正在关闭循环变量.不要那样做.取一份副本:

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);
}
Run Code Online (Sandbox Code Playgroud)

您当前的代码正在捕获path- 而不是创建任务时的,而是变量本身.每次进行循环时,该变量都会更改值 - 因此,在调用委托时,它可以轻松更改.

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

Eric Lippert有一对博客文章更详细地介绍了这一点:第1部分 ; 第2部分.

不要感觉不好 - 这几乎每个人都抓住了:(


关于这一行的说明:

task.ContinueWith(t => result &= t.Result);
Run Code Online (Sandbox Code Playgroud)

正如评论中指出的那样,这不是线程安全的.多个线程可以同时执行它,可能会对彼此的结果加以冲突.我没有添加锁定或任何类似的东西,因为它会分散该问题感兴趣的主要问题,即变量捕获.但是,值得注意的是.

  • @Snixtor:那仍然是C#5编译器.区分您正在使用的*language*版本和您要定位的*framework*版本非常重要. (4认同)
  • 此行为已更改,不仅在C#5.0(如Eric Lipperts博客文章的更新中所述)中有所更改,而且在VS2012中(如果您定位到4.0)已更改。 (2认同)
  • 此代码具有竞争条件,因为您将结果聚合到来自多个线程的`result`变量,而没有正确的同步. (2认同)

bdu*_*kes 12

您传递到拉姆达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);
}
Run Code Online (Sandbox Code Playgroud)