Идентификатор и замыкания 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();
}
Обновление: Как указано в ответе Джона Скита, это не имеет ничего общего с потоками.
Ответы
Ответ 1
Изменить: все изменения в С# 5, с изменением на то, где переменная определена (в глазах компилятора). Начиная с С# 5, они одинаковы.
Второй безопасен; первый - нет.
С foreach
переменная объявляется вне цикла - i.e.
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();
}
(каждый номер один раз, но, конечно, порядок не гарантируется)
Ответ 2
Ответы Pop Catalin и Marc Gravell верны. Все, что я хочу добавить, это ссылка на мою статью о закрытии (в которой говорится о Java и С#). Просто подумал, что это может добавить немного ценности.
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();
}
}
}
Ответ 3
Ваша потребность использовать параметр 2, создавая замыкание вокруг изменяющейся переменной, будет использовать значение переменной при использовании переменной, а не в момент создания закрытия.
Реализация анонимных методов в С# и его последствиях (часть 1)
Реализация анонимных методов в С# и его последствиях (часть 2)
Реализация анонимных методов в С# и его последствиях (часть 3)
Изменить: чтобы было ясно, что в С# закрытие " лексических замыканий" означает, что они не фиксируют значение переменной, а сама переменная. Это означает, что при создании замыкания на изменяющуюся переменную замыкание на самом деле является ссылкой на переменную, а не на копию ее значения.
Edit2: добавлены ссылки на все сообщения в блоге, если кто-то заинтересован в чтении о внутренних компонентах компилятора.
Ответ 4
Это интересный вопрос, и кажется, что мы видели, как люди отвечают разными способами. У меня создалось впечатление, что второй путь будет единственным безопасным способом. Я взломал реальное быстрое доказательство:
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 определенно небезопасен.
Ответ 5
В вашем случае вы можете избежать проблемы без использования трюка для копирования, сопоставив ваш ListOfFoo
с последовательностью потоков:
var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething()));
foreach (var t in threads)
{
t.Start();
}
Ответ 6
Оба являются безопасными с С# версии 5 (.NET framework 4.5). См. Этот вопрос для деталей: Изменено ли использование переменных foreach в С# 5?
Ответ 7
Foo f2 = f;
указывает на ту же ссылку, что и
f
Так что ничего не потеряно и ничего не получилось...