Identyfikator foreach i zamknięcia

W dwóch kolejnych fragmentach, czy pierwszy jest Bezpieczny, czy musisz zrobić drugi?

Przez safe mam na myśli czy każdy wątek ma zagwarantowane wywołanie metody na Foo z tej samej iteracji pętli, w której wątek został utworzony?

Czy musisz skopiować odniesienie 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();
}

Update: jak wskazano w odpowiedzi Jona Skeeta, nie ma to nic wspólnego z gwintowanie.

Author: abatishchev, 2009-02-04

7 answers

Edit: to wszystko zmienia się w C# 5, ze zmianą miejsca zdefiniowania zmiennej (w oczach kompilatora). Od C # 5, są takie same .


Przed C#5

Drugi jest bezpieczny, a pierwszy nie.

Z foreach zmienna jest deklarowana poza pętlą-tzn.

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

Oznacza to, że jest tylko 1 f pod względem zakresu zamknięcia, a wątki mogą się bardzo pomylić-wywołanie metody wiele razy w niektórych przypadkach, a nie w ogóle w innych. Można to naprawić za pomocą drugiej deklaracji zmiennej wewnątrz pętli:

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

To ma wtedy oddzielny tmp w każdym zakresie zamknięcia, więc nie ma ryzyka tego problemu.

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

Dodaj zmienną temp i działa:

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

(każda liczba raz, ale oczywiście kolejność nie jest gwarantowana)

 97
Author: Marc Gravell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-13 20:23:13

Odpowiedzi Pop Catalina i Marca Gravella są poprawne. Chcę tylko dodać link do mojego artykułu o closures (który mówi zarówno o Javie jak i C#). Pomyślałem, że to może dodać trochę wartości.

EDIT: 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ń" wyświetla się 10 razy; lista "dobrych działań" 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();
        }
    }
}
 34
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-02-04 17:32:23

Musisz użyć opcji 2, tworząc zamknięcie wokół zmieniającej się zmiennej użyje 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)

Edit: żeby było jasne, w C# closures są " leksykalne closures ", co oznacza, że nie przechwytują wartości zmiennej, ale samą zmienną. Oznacza to, że podczas tworzenia zamknięcia do zmieniającej się zmiennej zamknięcie jest w rzeczywistości odniesieniem do zmiennej, a nie kopią jej wartości.

Edit2: dodano linki do wszystkich postów na blogu, jeśli ktoś jest zainteresowany poczytaniem o wewnętrznych kompilatorach.

 16
Author: Pop Catalin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-02-04 17:27:24

To ciekawe pytanie i wydaje się, że widzieliśmy, jak ludzie odpowiadają na różne sposoby. Miałem wrażenie, że druga droga będzie jedyną bezpieczną drogą. Mam bardzo 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 jest zdecydowanie niebezpieczna.

 3
Author: JoshBerke,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-02-04 17:20:34

W Twoim przypadku możesz uniknąć problemu bez użycia sztuczki kopiowania, mapując ListOfFoo do sekwencji wątków:

var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething()));
foreach (var t in threads)
{
    t.Start();
}
 1
Author: Ben James,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-20 12:55:35

Oba są bezpieczne od wersji C# 5 (. NET framework 4.5). Zobacz to pytanie po szczegóły: Czy użycie zmiennych foreacha zostało zmienione w C# 5?

 0
Author: lex82,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:14:57
Foo f2 = f;

Wskazuje na to samo odniesienie co

f 
Więc nic straconego i nic zyskanego ...
 -5
Author: Matze,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-02-04 16:47:15