В двух следующих отрывках, является ли первый безопасным или вы должны сделать второй?
Под безопасностью я имею в виду, гарантированно ли каждый поток вызывает метод в 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();
}
Обновление: как указано в ответе Джона Скита, это не имеет никакого отношения к потокам.
Ответы:
Изменить: это все изменения в 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 #). Просто подумал, что это может добавить немного ценности.
РЕДАКТИРОВАТЬ: Я думаю, стоит привести пример, в котором нет непредсказуемости потоковой передачи. Вот короткая, но полная программа, показывающая оба подхода. Список «плохих действий» распечатывается 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(); } } }
источник
for
цикла, меня сбивает с толку. Например, в вашем примере поведения закрытия, stackoverflow.com/a/428624/20774 , переменная существует за пределами закрытия, но привязывается правильно. Почему это другое?Вам необходимо использовать вариант 2, создание замыкания вокруг изменяющейся переменной будет использовать значение переменной при использовании переменной, а не во время создания замыкания.
Реализация анонимных методов на C # и ее последствия (часть 1)
Реализация анонимных методов на C # и ее последствия (часть 2)
Реализация анонимных методов на C # и ее последствия (часть 3)
Изменить: чтобы было понятно, в 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). Подробнее см. В этом вопросе: изменилось ли использование переменных в C # 5 в foreach?
источник
указывает на ту же ссылку, что и
Так что ничего не потеряно и ничего не приобретено
источник