81 votes

L'identifiant foreach et les fermetures

Dans les deux extraits suivants, le premier est-il sûr ou devez-vous faire le second ?

Par "sûr", je veux dire que chaque thread a la garantie d'appeler la méthode sur le Foo à partir de la même itération de boucle dans laquelle le thread a été créé ?

Ou devez-vous 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 spécifiquement avec l'enfilage.

0 votes

En fait, je pense que cela a à voir avec le threading, car si vous n'utilisiez pas le threading, vous appelleriez le bon délégué. Dans l'exemple de Jon Skeet sans threading, le problème est qu'il y a 2 boucles. Ici, il n'y en a qu'une, donc il ne devrait pas y avoir de problème... sauf si vous ne savez pas exactement quand le code sera exécuté (c'est-à-dire si vous utilisez le threading - la réponse de Marc Gravell le montre parfaitement).

0 votes

Duplicata possible de Accès à la fermeture modifiée (2)

0 votes

@user276648 Cela ne nécessite pas de threading. Différer l'exécution des délégués jusqu'après la boucle serait suffisant pour obtenir ce comportement.

104voto

Marc Gravell Points 482669

Edit : tout cela change en C# 5, avec un changement de l'endroit où la variable est définie (aux yeux du compilateur). De A partir de C# 5, ils sont les mêmes .


Avant C#5

Le second est sûr, le premier ne l'est pas.

Avec foreach la variable est déclarée à l'extérieur de la boucle - c'est-à-dire

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

Cela signifie qu'il y a seulement 1 f en termes de portée de la fermeture, et les threads risquent fort de s'embrouiller - en appelant la méthode plusieurs fois sur certaines instances et pas du tout sur d'autres. Vous pouvez résoudre ce problème en déclarant une deuxième variable à l'intérieur de la boucle :

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

Celui-ci dispose alors d'une tmp dans chaque périmètre de fermeture, il n'y a donc aucun risque de ce problème.

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

Ajoutez une variable temporaire et ça marche :

        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 n'est pas garanti)

0 votes

Bon sang... ce vieux post m'a évité bien des maux de tête. Je m'attendais toujours à ce que la variable foreach soit scopée à l'intérieur de la boucle. C'était une expérience WTF majeure.

0 votes

En fait, cela a été considéré comme un bogue dans foreach-loop et corrigé dans le compilateur. (Contrairement à la boucle for où la variable a une seule instance pour toute la boucle).

0 votes

@Orlangur J'ai eu des conversations directes avec Eric, Mads et Anders à ce sujet pendant des années. Le compilateur a suivi la spécification et avait donc raison. La spécification a fait un choix. Simplement : ce choix a été changé.

37voto

Jon Skeet Points 692016

Les réponses de Pop Catalin et Marc Gravell sont correctes. Tout ce que je veux ajouter, c'est un lien vers mon article sur les fermetures (qui traite à la fois de Java et de C#). J'ai juste pensé que cela pourrait ajouter un peu de valeur.

EDIT : Je pense que cela vaut la peine de donner un exemple qui n'a pas l'imprévisibilité du threading. Voici un programme court mais complet qui montre les deux approches. La liste des "mauvaises actions" imprime 10 dix fois ; la liste des "bonnes actions" 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();
        }
    }
}

1 votes

Merci - j'ai ajouté la question pour dire qu'il ne s'agit pas vraiment de fils.

0 votes

C'était également dans l'une des conférences que vous avez en vidéo sur votre site. csharpindepth.com/Talks.aspx

0 votes

Oui, je crois me souvenir que j'ai utilisé une version de filage là, et l'une des suggestions de retour d'information était d'éviter les fils - il est plus clair d'utiliser un exemple comme celui ci-dessus.

17voto

Pop Catalin Points 25033

Vous devez utiliser l'option 2, la création d'une fermeture autour d'une variable changeante utilisera la valeur de la variable lorsque celle-ci est utilisée et non au moment de la création de la fermeture.

L'implémentation de méthodes anonymes en C# et ses conséquences (partie 1)

L'implémentation de méthodes anonymes en C# et ses conséquences (partie 2)

L'implémentation de méthodes anonymes en C# et ses conséquences (partie 3)

Edit : pour être clair, en C# les fermetures sont " fermetures lexicales "Ce qui signifie qu'ils ne capturent pas la valeur d'une variable mais la variable elle-même. Cela signifie que lorsque l'on crée une fermeture pour une variable changeante, la fermeture est en fait une référence à la variable et non une copie de sa valeur.

Edit2 : ajout de liens vers tous les articles du blog si quelqu'un est intéressé par la lecture des internes du compilateur.

0 votes

Je pense que cela vaut pour les types de valeurs et de références.

3voto

JoshBerke Points 34238

C'est une question intéressante et il semble que nous ayons vu des gens répondre de toutes sortes de façons. J'avais l'impression que la deuxième façon était la seule sûre. J'ai fait une 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 ceci, vous verrez que l'option 1 n'est définitivement pas sûre.

1voto

Ben James Points 41165

Dans votre cas, vous pouvez éviter le problème sans avoir recours à l'astuce de la copie en faisant correspondre votre fichier ListOfFoo à une séquence de fils :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X