Domanda

Nei due seguenti frammenti, è il primo sicuro o si deve fare il secondo?

Per sicuro intendo è ogni thread garantita chiamare il metodo sulla Foo dal medesimo iterazione del ciclo in cui è stato creato il filo?

In alternativa si deve copiare il riferimento ad una nuova variabile "locale" ad ogni iterazione del ciclo?

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

Aggiornamento:. Come sottolineato nella risposta di Jon Skeet, questo non ha nulla a che fare con specifico filettatura

È stato utile?

Soluzione

Edit: questo tutti i cambiamenti in C # 5, con una modifica in cui è definita la variabile (agli occhi del compilatore). Da C # 5 in poi, sono la stessa .


Prima di C # 5

Il secondo è sicura; il primo non è.

Con foreach, la variabile è dichiarata fuori il loop -. Cioè

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

Questo significa che c'è solo 1 f in termini di portata di chiusura, ei fili potrebbe molto probabilmente confondersi - chiamando il metodo più volte su alcuni casi e non a tutti gli altri. È possibile risolvere questo con una seconda dichiarazione di variabile all'interno il ciclo:

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

Questo ha poi un tmp separato in ogni portata chiusura, quindi non c'è rischio di questo problema.

Ecco una semplice dimostrazione del problema:

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

uscite (a caso):

1
3
4
4
5
7
7
8
9
9

Aggiungere una variabile temporanea e funziona:

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

(ogni numero una volta, ma ovviamente l'ordine non garantita)

Altri suggerimenti

Pop Catalin e le risposte di Marc Gravell siano corrette. Tutto quello che voglio aggiungere è un link a il mio articolo su chiusure (che parla sia Java e C#). Proprio pensato che potrebbe aggiungere un po 'di valore.

EDIT: Penso che vale la pena dare un esempio che non ha l'imprevedibilità di filettatura. Ecco un programma breve ma completo che mostra entrambi gli approcci. La lista "cattiva azione" stampa le 10 dieci volte; la lista "buona azione" conta da 0 a 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();
        }
    }
}

La vostra necessità di utilizzare l'opzione 2, creando una chiusura attorno una variabile che cambia utilizzerà il valore della variabile quando la variabile viene utilizzata e non al momento della creazione di chiusura.

L'attuazione di metodi anonimi in C # e le sue conseguenze (parte 1)

L'attuazione di metodi anonimi in C # e le sue conseguenze (parte 2)

L'attuazione di metodi anonimi in C # e le sue conseguenze (parte 3)

Modifica: per chiarire, in C # chiusure sono " chiusure lessicali " nel senso che non catturano il valore di una variabile, ma la variabile stessa. Ciò significa che quando si crea una chiusura a una variabile che cambia la chiusura è in realtà un riferimento alla variabile non una copia del suo valore.

Edit2:. Collegamenti aggiunto a tutti i post del blog, se qualcuno è interessato a leggere su interni del compilatore

Questa è una domanda interessante e sembra che abbiamo visto le persone rispondono in tutti i vari modi. Ho avuto l'impressione che il secondo modo sarebbe l'unico modo sicuro. Ho sbattuto una vera e propria prova di rapida:

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

se si esegue questo si vedrà l'opzione 1 non è sicuramente al sicuro.

Nel vostro caso, è possibile evitare il problema senza utilizzare il trucco copia mappando il tuo ListOfFoo ad una sequenza di fili:

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

Entrambi sono sicuri come di C # versione 5 (NET Framework 4.5). Vedere questa domanda per i dettagli: ha l'uso del foreach di variabili stato cambiato in C # 5?

Foo f2 = f;

indica lo stesso riferimento

f 

Quindi niente di perduto e non rosica ...

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top