Идентификатор foreach и замыкания
в двух следующих фрагментах, является первым безопасным или вы должны сделать второй?
под безопасным я имею в виду, что каждый поток гарантированно вызывает метод на Foo из той же итерации цикла, в которой был создан поток?
или вы должны скопировать ссылку на новую переменную "local" для каждой итерации цикла?
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();
}
обновление: как указано в ответе Джона Скита, в этом нет ничего в частности, чтобы сделать с резьбой.
7 ответов:
Edit: это все изменения в C# 5, с изменением того, где переменная определена (в глазах компилятора). От C# 5 вперед, они одинаковы.
Перед C#5
второй безопасен; первый нет.
С
foreachпеременная объявлена за пределами цикл - т. е.Foo f; while(iterator.MoveNext()) { f = iterator.Current; // do something with f }это означает, что есть только 1
fпо отоношению к объему закрытия, и потоки, скорее всего, могут запутаться - вызов метода несколько раз на некоторых экземплярах, а не на всех других. Вы можете исправить это с помощью второго объявления переменной внутри петли: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(); }(каждый номер один раз, но, конечно, порядок не гарантируется)
ответы попа Каталина и Марка Гравелла верны. Все, что я хочу добавить, это ссылка на моя статья о замыканиях (что говорит как о Java, так и о C#). Просто подумал, что это может добавить немного ценности.
EDIT: я думаю, что стоит привести пример, который не имеет непредсказуемости резьбы. Вот короткая, но полная программа, показывающая оба подхода. Список " плохое действие "печатает 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, создавая закрытие вокруг изменяющейся переменной будет использовать значение переменной, когда переменная используется, а не во время создания закрытия.
реализация анонимных методов в C# и ее последствия (часть 1)
реализация анонимных методов в C# и ее последствия (часть 2)
реализация анонимных методов в C# и ее последствия (часть 3)
Edit: чтобы было понятно, в C# замыкания"лексические замыкания " это означает, что они не захватывают значение переменной, а саму переменную. Это означает, что при создании закрытия изменяющейся переменной закрытие фактически является ссылкой на переменную, а не копией ее значения.
Edit2: добавлены ссылки на все сообщения в блоге, если кто-то заинтересован в чтении о внутренних компонентах компилятора.
Это интересный вопрос, и кажется, что мы видели, как люди отвечают по-разному. У меня сложилось впечатление, что второй путь будет единственным безопасным. Я хлестнул очень быстрое доказательство:
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 framework 4.5). Смотрите этот вопрос для деталей:было ли изменено использование переменных foreach в C# 5?
Foo f2 = f;указывает на ту же ссылку, что и
fТак что ничего не потеряно и ничего не приобретено ...
Comments