2009-02-04 2 views
73

В двух следующих фрагментах, является первым безопасным или вы должны сделать второй?Идентификатор и замыкания 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(); 
} 

Update: Как указано в ответе Джона Скита, это не имеет ничего специально делать с резьбой.

+0

На самом деле, я чувствую, что он имеет дело с потоками, как если бы вы не использовали потоки, вы бы назвали нужным делегатом. В примере с Джоном Скитом без потоковой обработки проблема состоит в том, что существует 2 цикла. Вот только один, поэтому не должно быть никаких проблем ... если вы точно не знаете, когда будет выполняться код (что означает, что если вы используете потоки - ответ Марка Гравелла показывает, что это прекрасно). – user276648

+0

Возможный дубликат [Доступ к измененному закрытию (2)] (http://stackoverflow.com/questions/304258/access-to-modified-closure-2) – nawfal

+0

@ user276648 Он не требует потоковой передачи. Отложите выполнение делегатов до тех пор, пока цикл не будет достаточным для получения этого поведения. – binki

ответ

97

Редактировать: все это изменяется в 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(); 
     } 

(каждый номер один раз, но, разумеется, заказ не гарантируется)

+0

Спасибо, Марк. Хороший пост. – xyz

+0

Святая корова ... эта старая почта спасла мне много головной боли. Я всегда ожидал, что переменная foreach будет находиться внутри цикла. Это был один крупный опыт WTF. – chris

+0

На самом деле это считалось ошибкой в ​​foreach-loop и исправлено в компиляторе. (В отличие от for-loop, где переменная имеет единственный экземпляр для всего цикла.) –

-5
Foo f2 = f; 

указывает на то же ссылке как

f 

Так что ничего потеряно, и ничего не получил ...

+0

Дело в том, что компилятор здесь выполняет некоторую магию, чтобы поднять локальную переменную в область неявно созданного закрытия. Вопрос заключается в том, что компилятор понимает это для переменной цикла. Компилятор известен несколькими недостатками относительно подъема, поэтому это хороший вопрос. –

+1

Это не волшебство. Он просто захватывает окружающую среду. Проблема здесь и с циклами в том, что переменная захвата становится мутированной (переопределенной). – leppie

+1

leppie: компилятор генерирует код для вас, и это не легко увидеть вообще * какой * код именно это. Это * * определение магии компилятора, если оно когда-либо было. –

16

Ваша потребность использовать вариант 2, создавая замыкание вокруг изменяющейся переменной будет использовать значение переменной, когда используется переменная, а не время создания закрытия.

The implementation of anonymous methods in C# and its consequences (part 1)

The implementation of anonymous methods in C# and its consequences (part 2)

The implementation of anonymous methods in C# and its consequences (part 3)

Edit: чтобы было понятно, в C# закрытия являются "лексические замыкания" означает, что они не учитывают значение переменной, но саму переменную. Это означает, что при создании замыкания на изменяющуюся переменную замыкание на самом деле является ссылкой на переменную, а не на копию ее значения.

Edit2: ссылки на все сообщения в блоге, если кто-то заинтересован в чтении о внутренних компонентах компилятора.

+0

Я думаю, что это касается значений и ссылочных типов. – leppie

33

Ответы поп-Каталина и Марка Гравелла верны. Все, что я хочу добавить, это ссылка на my article about closures (в которой говорится о 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(); 
     } 
    } 
} 
+1

Спасибо - я приложил вопрос, чтобы сказать, что речь идет не о потоках. – xyz

+0

Это было также в одном из разговоров, которые у вас есть на видео на вашем сайте http://csharpindepth.com/Talks.aspx – rizzle

+0

Да, я, кажется, помню, что я использовал поточную версию там, и одно из предложений отзывов было избегайте потоков - лучше использовать пример, подобный приведенному выше. –

3

Это интересный вопрос, и это, кажется, как мы видели, как люди отвечают на всех по-разному. У меня создалось впечатление, что второй путь будет единственным безопасным способом. Я взломал реальное быстрое доказательство:

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 определенно небезопасен.

+0

Спасибо за полную программу :) – xyz

1

В вашем случае, вы можете избежать проблем без использования копировальной трюк отображения вашего ListOfFoo последовательности нитей:

var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething())); 
foreach (var t in threads) 
{ 
    t.Start(); 
} 
Смежные вопросы