79

在以下两个片段中,第一个是安全的还是必须做第二个?

安全我的意思是每个线程都保证从创建线程的同一个循环迭代中调用 Foo 上的方法?

还是必须将对新变量“本地”的引用复制到循环的每次迭代中?

var threads = new List<Thread>();
foreach (Foo f in ListOfFoo)
{      
    Thread thread = new Thread(() => f.DoSomething());
    threads.Add(thread);
    thread.Start();
}

-

var threads = new List<Thread>();
foreach (Foo f in ListOfFoo)
{      
    Foo f2 = f;
    Thread thread = new Thread(() => f2.DoSomething());
    threads.Add(thread);
    thread.Start();
}

更新:正如 Jon Skeet 的回答所指出的,这与线程没有任何关系。

4

7 回答 7

103

编辑:这一切都在 C# 5 中发生了变化,改变了变量的定义位置(在编译器的眼中)。从C# 5 开始,它们是相同的.


在 C#5 之前

二是安全;第一个不是。

使用foreach,变量在循环声明- 即

Foo f;
while(iterator.MoveNext())
{
     f = iterator.Current;
    // do something with f
}

这意味着就f闭包范围而言只有 1 个,并且线程很可能会感到困惑——在某些实例上多次调用该方法,而在其他实例上则根本不调用。您可以使用循环的第二个变量声明来解决此问题:

foreach(Foo f in ...) {
    Foo tmp = f;
    // do something with tmp
}

然后,这在每个闭包范围中都有一个单独tmp的,因此不存在此问题的风险。

这是问题的简单证明:

    static void Main()
    {
        int[] data = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        foreach (int i in data)
        {
            new Thread(() => Console.WriteLine(i)).Start();
        }
        Console.ReadLine();
    }

输出(随机):

1
3
4
4
5
7
7
8
9
9

添加一个临时变量,它可以工作:

        foreach (int i in data)
        {
            int j = i;
            new Thread(() => Console.WriteLine(j)).Start();
        }

(每个数字一次,但当然不能保证顺序)

于 2009-02-04T17:02:01.653 回答
37

Pop Catalin 和 Marc Gravell 的答案是正确的。我只想添加一个链接,指向我关于闭包的文章(其中讨论了 Java 和 C#)。只是觉得它可能会增加一点价值。

编辑:我认为值得举一个不具有线程不可预测性的示例。这是一个简短但完整的程序,展示了这两种方法。“不良行为”列表打印 10 次十次;“好行为”列表从 0 到 9 计数。

using System;
using System.Collections.Generic;

class Test
{
    static void Main() 
    {
        List<Action> badActions = new List<Action>();
        List<Action> goodActions = new List<Action>();
        for (int i=0; i < 10; i++)
        {
            int copy = i;
            badActions.Add(() => Console.WriteLine(i));
            goodActions.Add(() => Console.WriteLine(copy));
        }
        Console.WriteLine("Bad actions:");
        foreach (Action action in badActions)
        {
            action();
        }
        Console.WriteLine("Good actions:");
        foreach (Action action in goodActions)
        {
            action();
        }
    }
}
于 2009-02-04T17:17:36.770 回答
17

您需要使用选项 2,围绕变化的变量创建闭包将在使用变量时使用变量的值,而不是在创建闭包时使用。

C# 中匿名方法的实现及其后果(第 1 部分)

C# 中匿名方法的实现及其后果(第 2 部分)

C# 中匿名方法的实现及其后果(第 3 部分)

编辑:为了清楚起见,在 C# 中,闭包是“词法闭包”,这意味着它们不捕获变量的值,而是捕获变量本身。这意味着当为变化的变量创建闭包时,闭包实际上是对变量的引用,而不是其值的副本。

Edit2:如果有人有兴趣阅读有关编译器内部的内容,则添加指向所有博客文章的链接。

于 2009-02-04T16:57:52.333 回答
3

这是一个有趣的问题,似乎我们已经看到人们以各种方式回答。我的印象是第二种方式将是唯一安全的方式。我做了一个真正的快速证明:

class Foo
{
    private int _id;
    public Foo(int id)
    {
        _id = id;
    }
    public void DoSomething()
    {
        Console.WriteLine(string.Format("Thread: {0} Id: {1}", Thread.CurrentThread.ManagedThreadId, this._id));
    }
}
class Program
{
    static void Main(string[] args)
    {
        var ListOfFoo = new List<Foo>();
        ListOfFoo.Add(new Foo(1));
        ListOfFoo.Add(new Foo(2));
        ListOfFoo.Add(new Foo(3));
        ListOfFoo.Add(new Foo(4));


        var threads = new List<Thread>();
        foreach (Foo f in ListOfFoo)
        {
            Thread thread = new Thread(() => f.DoSomething());
            threads.Add(thread);
            thread.Start();
        }
    }
}

如果你运行它,你会看到选项 1 绝对不安全。

于 2009-02-04T17:20:34.800 回答
1

在您的情况下,您可以通过将您映射ListOfFoo到一系列线程来避免该问题,而无需使用复制技巧:

var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething()));
foreach (var t in threads)
{
    t.Start();
}
于 2011-12-20T12:55:35.110 回答
0

从 C# 版本 5(.NET 框架 4.5)开始,两者都是安全的。有关详细信息,请参阅此问题:C# 5 中是否更改了 foreach 对变量的使用?

于 2015-12-18T17:38:33.067 回答
-5
Foo f2 = f;

指向相同的参考

f 

所以什么都没有失去,也没有任何收获......

于 2009-02-04T16:47:15.803 回答