foreach标识符和closures
在下面的两个片段中,第一个是安全还是第二个?
通过安全我的意思是每个线程保证从线程创build相同的循环迭代调用Foo的方法?
或者你必须复制引用到一个新的variables“本地”到循环的每个迭代?
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的回答所指出的那样,这与线程无关。
编辑:这一切在C#5中的变化,改变了variables定义的地方(在编译器的眼中)。 从C#5开始,它们是一样的。
第二是安全的; 第一个不是。
用foreach
,这个variables是在循环之外声明的 – 也就是说
Foo f; while(iterator.MoveNext()) { f = iterator.Current; // do something with f }
这意味着closures范围只有1 f
,线程可能很容易混淆 – 在某些情况下多次调用该方法,而在其他情况下不调用该方法。 你可以在循环中使用第二个variables声明来解决这个问题:
foreach(Foo f in ...) { Foo tmp = f; // do something with tmp }
这在每个闭包范围内都有一个单独的tmp
,所以不存在这个问题的风险。
这是一个简单的问题certificate:
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
添加一个临时variables,它的工作原理:
foreach (int i in data) { int j = i; new Thread(() => Console.WriteLine(j)).Start(); }
(每个号码一次,但当然,顺序不能保证)
Pop Catalin和Marc Gravell的回答是正确的。 我想添加的是链接到我的文章关于闭包 (谈论Java和C#)。 只是认为它可能会增加一点价值。
编辑:我觉得值得给一个没有线程不可预测性的例子。 这是一个简短但完整的程序,展示了两种方法。 “不良行为”列表打印10次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(); } } }
您需要使用选项2,在variablesvariables周围创build闭包时,将使用variables的值,而不是在创build闭包时使用variables的值。
C#中匿名方法的实现及其后果(第1部分)
C#中匿名方法的实现及其后果(第2部分)
C#中匿名方法的实现及其后果(第3部分)
编辑:清楚地说,在C#中,闭包是“ 词法闭包 ”,意思是它们不捕获variables的值,而是variables本身。 这意味着当为一个variablesvariables创build一个闭包时,闭包实际上是对variables的引用,而不是其值的副本。
Edit2:如果有人有兴趣阅读关于编译器内部的知识,可以添加所有博客文章的链接。
这是一个有趣的问题,我们看到人们似乎以各种方式回答。 我的印象是第二种方式是唯一安全的方式。 我掀起了一个真正的快速certificate:
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是不安全的。
就你而言,你可以通过将ListOfFoo
映射到一系列线程来避免这个问题,而不使用复制技巧:
var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething())); foreach (var t in threads) { t.Start(); }
C#版本5(.NET框架4.5)都是安全的。 看到这个问题的细节: 在C#5 foreach的variables使用已被改变?
Foo f2 = f;
指向与之相同的引用
f
所以没有什么损失,也没有增加