Идентификатор foreach и замыкания
-
21-08-2019 - |
Вопрос
В двух следующих фрагментах безопасен первый или необходимо выполнить второй?
Под безопасностью я имею в виду, гарантированно ли каждый поток вызывает метод 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();
}
Обновлять: Как указано в ответе Джона Скита, это не имеет ничего общего с многопоточностью.
Решение
Редактировать:все это изменилось в C# 5 с изменением места определения переменной (в глазах компилятора).От C# 5 и выше, они одинаковы.
До С#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();
}
}
}
Вам нужно использовать вариант 2: при создании замыкания вокруг изменяющейся переменной будет использоваться значение переменной, когда переменная используется, а не во время создания замыкания.
Реализация анонимных методов в C# и ее последствия (часть 1)
Реализация анонимных методов в C# и ее последствия (часть 2)
Реализация анонимных методов в C# и ее последствия (часть 3)
Редактировать:чтобы было понятно, в C# замыкания "лексические замыкания" это означает, что они фиксируют не значение переменной, а саму переменную.Это означает, что при создании замыкания изменяющейся переменной замыкание на самом деле является ссылкой на переменную, а не копией ее значения.
Редактировать2:добавлены ссылки на все сообщения в блоге, если кому-то интересно узнать о внутреннем устройстве компилятора.
Это интересный вопрос, и кажется, что мы видели, как люди отвечали по-разному.У меня сложилось впечатление, что второй путь будет единственным безопасным.Я привел очень быстрое доказательство:
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
Так что ничего не потеряно и ничего не приобретено...