Question

Dans les deux extraits suivants, est le premier coffre-fort ou devez-vous faire le second?

Je veux dire par la sécurité est chaque fil garanti pour appeler la méthode sur la Foo de la même itération de la boucle dans laquelle le fil a été créé?

Ou vous devez copier la référence à une nouvelle variable « locale » à chaque itération de la boucle?

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

Mise à jour:. Comme indiqué dans la réponse de Jon Skeet, cela n'a rien à voir avec spécifiquement filetage

Était-ce utile?

La solution

Edit: ce toutes les modifications en C # 5, avec un changement à l'endroit où la variable est définie (dans les yeux du compilateur). De C # 5 partir, ils sont les mêmes .


Avant C # 5

Le second est sûr; le premier n'est pas.

Avec foreach, la variable est déclarée extérieur la boucle -. I.e.

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

Cela signifie qu'il ya seulement 1 en termes de f la portée de fermeture, et les fils pourrait très probablement se confondre - appeler la méthode à plusieurs reprises sur certains cas et pas du tout sur les autres. Vous pouvez résoudre ce problème avec une deuxième déclaration de variable dans la boucle:

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

a alors séparé dans chaque tmp champ de fermeture, donc il n'y a pas de risque de cette question.

Voici une preuve simple du problème:

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

Sorties (au hasard):

1
3
4
4
5
7
7
8
9
9

Ajouter une variable temp et il fonctionne:

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

(chaque numéro une fois, mais bien sûr, l'ordre est pas garanti)

Autres conseils

Pop Catalin et les réponses de Marc Gravell sont corrects. Tout ce que je veux ajouter est un lien vers mon article sur les fermetures (qui parle à la fois Java et C#). Juste pensé que cela pourrait ajouter un peu de valeur.

EDIT: Je pense qu'il est utile de donner un exemple qui n'a pas le caractère imprévisible du filetage. Voici un programme court mais complet montrant les deux approches. La liste « mauvaise action » imprime 10 dix fois; la liste "bonne action" compte de 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();
        }
    }
}

Votre besoin d'utiliser l'option 2, la création d'une fermeture autour d'une variable changeant utilisera la valeur de la variable lorsque la variable est utilisée et non au moment de la création de fermeture.

La mise en œuvre des méthodes anonymes en C # et ses conséquences (partie 1)

La mise en œuvre des méthodes anonymes en C # et ses conséquences (partie 2)

La mise en œuvre des méthodes anonymes en C # et ses conséquences (partie 3)

Edit: à préciser, dans les fermetures C # sont « fermetures lexicales » ce qui signifie qu'ils ne saisissent pas la valeur d'une variable, mais la variable elle-même. Cela signifie que lors de la création d'une fermeture à une variable changer la fermeture est en fait une référence à la variable non une copie de sa valeur.

Edit2. Liens ajouté à tous les messages de blog si quelqu'un est intéressé par la lecture sur internes du compilateur

Ceci est une question intéressante et il semble que nous avons vu des gens répondent à toutes les différentes façons. J'avais l'impression que la deuxième voie serait la seule façon sûre. Je fouetté une vraie preuve rapide:

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

si vous exécutez cela, vous verrez l'option 1 est definetly pas sûr.

Dans votre cas, vous pouvez éviter le problème sans utiliser l'astuce de la copie par mapper à une séquence ListOfFoo de fils:

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

Les deux sont sûrs que la version C # 5 (le framework .NET 4.5). Voir cette question pour plus de détails: A l'utilisation de foreach les variables été changées en C # 5

Foo f2 = f;

pointe vers la même référence que

f 

Alors rien perdu et rien gagné ...

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top