2009-02-04 16 views
73

W dwóch poniższych fragmentach pierwszy z nich jest bezpieczny lub czy należy wykonać drugi?Identyfikator foreach i zamknięcia

Czy to bezpieczne, że każdy wątek gwarantuje wywołanie metody na Foo z tej samej iteracji pętli, w której wątek został utworzony?

Czy trzeba skopiować referencję do nowej zmiennej "local" do każdej iteracji pętli?

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(); 
} 

Aktualizacja: Jak wskazano w odpowiedzi Jon Skeet, ten nie ma nic wspólnego ze specjalnie gwintowania.

+0

Właściwie uważam, że ma to związek z wątkami, tak jakbyś nie korzystał z wątków, nazwałbyś właściwego delegata. W próbce Jona Skeeta bez wątków, problem polega na tym, że istnieją 2 pętle. Tutaj jest tylko jeden, więc nie powinno być problemu ... chyba że nie wiesz dokładnie, kiedy kod zostanie wykonany (co oznacza, że ​​jeśli używasz wątków - odpowiedź Marc Gravell pokazuje to doskonale). – user276648

+0

możliwy duplikat [Dostęp do zmodyfikowanego zamknięcia (2)] (http://stackoverflow.com/questions/304258/access-to-modified-closure-2) – nawfal

+0

@ user276648 Nie wymaga gwintowania. Odroczenie wykonania delegatów, dopóki pętla nie wystarczy, aby uzyskać to zachowanie. – binki

Odpowiedz

97

Edytuj: to wszystkie zmiany w C# 5, ze zmianą w miejscu, gdzie zmienna jest zdefiniowana (w oczach kompilatora). Od C# 5 są one takie same.


Drugi jest bezpieczny; pierwszy nie jest.

Z foreach, zmienna jest zadeklarowana poza pętlę - tzn

Foo f; 
while(iterator.MoveNext()) 
{ 
    f = iterator.Current; 
    // do something with f 
} 

Oznacza to, że istnieje tylko 1 f pod względem zakresu zamknięcia, a gwinty mogą najprawdopodobniej się mylić - wywołanie metoda wielokrotnie w niektórych przypadkach, a nie w innych. Można rozwiązać ten problem z drugim deklaracji zmiennej wewnątrz pętlę:

foreach(Foo f in ...) { 
    Foo tmp = f; 
    // do something with tmp 
} 

To wtedy ma osobny tmp w każdym zakresie zamknięcia, więc nie ma ryzyka z tym problemem.

Oto prosty dowód problemu:

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(); 
    } 

Wyjścia (losowo):

1 
3 
4 
4 
5 
7 
7 
8 
9 
9 

dodać zmienną temp i to działa:

 foreach (int i in data) 
     { 
      int j = i; 
      new Thread(() => Console.WriteLine(j)).Start(); 
     } 

(każdy numer jeden raz, ale oczywiście zamówienie nie jest gwarantowane)

+0

Dziękuję, Marc. Niezły post. – xyz

+0

Święta krowa ... ten stary post zaoszczędził mi wielu bólów głowy. Zawsze spodziewałem się, że zmienna foreach ma zasięg wewnątrz pętli. To było jedno z największych doświadczeń WTF. – chris

+0

Właściwie to zostało uznane za błąd w pętli foreach i naprawione w kompilatorze. (W przeciwieństwie do pętli for, w której zmienna ma jedno wystąpienie dla całej pętli.) –

-5
Foo f2 = f; 

punkty do tego samego odniesienia jako

f 

Więc nic nie stracił, a nic nie zyskał ...

+0

Chodzi o to, że kompilator wykonuje tutaj trochę magii, aby podnieść lokalną zmienną do zakresu niejawnie utworzonego zamknięcia. Pytanie brzmi, czy kompilator rozumie, że robi to dla zmiennej pętli. Kompilator znany jest z kilku słabych stron dotyczących podnoszenia, więc jest to dobre pytanie. –

+1

To nie jest magia. Po prostu wychwytuje środowisko. Problem tutaj i pętli for, polega na tym, że zmienna przechwytywania zostaje zmutowana (ponownie przydzielona). – leppie

+1

leppie: kompilator generuje kod dla ciebie i nie jest łatwo zobaczyć ogólnie * jaki * kod to jest dokładnie. To jest * definicja * magii kompilatora, jeśli kiedykolwiek była. –

16

swoją potrzebę użycia opcji 2, tworząc zamknięcie wokół zmieniającym zmiennej użyje wartość zmiennej, gdy używana jest zmienna, a nie w momencie tworzenia zamknięcia.

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)

Edycja: aby było jasne, w C# zamknięć są "zamknięcia leksykalne", co oznacza, że ​​nie uchwycić wartość zmiennej ale sama zmienna. Oznacza to, że podczas tworzenia zamknięcia zmiennej zmieniającej zamknięcie jest w rzeczywistości odniesieniem do zmiennej, a nie kopii jej wartości.

Edycja2: dodano linki do wszystkich wpisów na blogu, jeśli ktoś jest zainteresowany czytaniem o wewnętrznych kompilacjach.

+0

Myślę, że chodzi o wartości i typy referencyjne. – leppie

33

Odpowiedzi Pop Catalina i Marca Gravella są poprawne. Wszystko, co chcę dodać, to link do my article about closures (który mówi zarówno o Javie, jak i C#). Pomyślałem, że może dodać trochę wartości.

EDYCJA: Myślę, że warto dać przykład, który nie ma nieprzewidywalności wątków. Oto krótki, ale kompletny program pokazujący oba podejścia. Lista "Zła akcja" wyświetla 10 razy dziesięć; lista „dobry uczynek” liczy się od 0 do 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

Dzięki - dołączyłem pytanie, aby powiedzieć, że tak naprawdę nie chodzi o wątki. – xyz

+0

Było to również w jednej z rozmów, które masz na wideo na swojej stronie http://csharpindepth.com/Talks.aspx – rizzle

+0

Tak, wydaje mi się, że pamiętam, użyłem tam wersji wątków, a jedną z sugestii było unikaj wątków - bardziej przejrzyste jest użycie przykładu podobnego do powyższego. –

3

To jest ciekawe pytanie i wydaje się, że widzieliśmy ludzi odpowie na wszystkie różne sposoby. Miałem wrażenie, że druga droga byłaby jedyną bezpieczną drogą. Udało mi się uzyskać szybki dowód:

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(); 
     } 
    } 
} 

Jeśli to uruchomisz zobaczysz, że opcja 1 nie jest bezpieczna.

+0

Dzięki za pełny program :) – xyz

1

W twoim przypadku można uniknąć problemu bez użycia trick kopiowania przez mapowanie ListOfFoo sekwencji wątków:

var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething())); 
foreach (var t in threads) 
{ 
    t.Start(); 
}