Czy w dwóch kolejnych fragmentach pierwszy z nich jest bezpieczny, czy też musisz zrobić drugi?
Przez bezpieczne rozumiem, czy każdy wątek gwarantuje wywołanie metody na Foo z tej samej iteracji pętli, w której został utworzony wątek?
A może musisz skopiować odniesienie do nowej zmiennej „lokalnej” 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 Jona Skeeta, nie ma to nic wspólnego z wątkami.
c#
enumeration
closures
xyz
źródło
źródło
Odpowiedzi:
Edycja: to wszystko zmienia się w C # 5, ze zmianą miejsca zdefiniowania zmiennej (w oczach kompilatora). Począwszy od C # 5 są one takie same .
Przed C # 5
Drugi jest bezpieczny; pierwszy nie jest.
W
foreach
przypadku zmiennej deklarowana jest poza pętlą - tjFoo 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 wątki mogą być bardzo zdezorientowane - wywołując metodę wiele razy w niektórych instancjach, a wcale nie w innych. Możesz to naprawić za pomocą drugiej deklaracji zmiennej w środku pętli:foreach(Foo f in ...) { Foo tmp = f; // do something with tmp }
To następnie ma oddzielny
tmp
zakres w każdym zamknięciu, więc nie ma ryzyka tego problemu.Oto prosty dowód na problem:
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
Dodaj zmienną tymczasową i działa:
foreach (int i in data) { int j = i; new Thread(() => Console.WriteLine(j)).Start(); }
(każdy numer raz, ale oczywiście kolejność nie jest gwarantowana)
źródło
Odpowiedzi Pop Catalina i Marca Gravella są poprawne. Chcę tylko dodać link do mojego artykułu o zamknięciach (który mówi zarówno o Javie, jak i C #). Pomyślałem, że może to dodać trochę wartości.
EDYCJA: Myślę, że warto podać przykład, który nie ma nieprzewidywalności wątków. Oto krótki, ale kompletny program pokazujący oba podejścia. Lista „złych działań” jest drukowana 10 razy; lista „dobrych działań” liczy 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(); } } }
źródło
for
pętlą, to zachowanie jest dla mnie mylące. Na przykład w Twoim przykładzie zachowania zamknięcia, stackoverflow.com/a/428624/20774 , zmienna istnieje poza zamknięciem, ale wiąże się poprawnie. Dlaczego jest inaczej?Musisz użyć opcji 2, utworzenie zamknięcia wokół zmieniającej się zmiennej spowoduje użycie wartości zmiennej, gdy zmienna jest używana, a nie w czasie tworzenia zamknięcia.
Implementacja metod anonimowych w C # i jej konsekwencje (część 1)
Implementacja metod anonimowych w C # i jej konsekwencje (część 2)
Implementacja metod anonimowych w C # i jej konsekwencje (część 3)
Edycja: aby było jasne, w C # domknięcia są „ zamknięciami leksykalnymi ”, co oznacza, że nie przechwytują wartości zmiennej, ale samą zmienną. Oznacza to, że podczas tworzenia domknięcia zmiennej zmiennej zamknięcie jest w rzeczywistości odniesieniem do zmiennej, a nie kopią jej wartości.
Edit2: dodane linki do wszystkich postów na blogu, jeśli ktoś jest zainteresowany czytaniem o wewnętrznych funkcjach kompilatora.
źródło
To interesujące pytanie i wydaje się, że widzieliśmy, jak ludzie odpowiadają na wiele różnych sposobów. Miałem wrażenie, że druga droga będzie jedyną bezpieczną. Przygotowałem naprawdę 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 na pewno nie jest bezpieczna.
źródło
W twoim przypadku możesz uniknąć problemu bez korzystania ze sztuczki kopiowania, mapując swój
ListOfFoo
do sekwencji wątków:var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething())); foreach (var t in threads) { t.Start(); }
źródło
Oba są bezpieczne od wersji C # 5 (.NET Framework 4.5). Zobacz to pytanie, aby uzyskać szczegółowe informacje: Czy użycie zmiennych foreach zostało zmienione w C # 5?
źródło
wskazuje na to samo odniesienie co
Więc nic straconego i nic nie zyskało ...
źródło