2009-02-04 15 views
73

En los dos fragmentos siguientes, ¿es seguro el primero o debe hacer el segundo?El identificador de foreach y cierres

Con seguridad quiero decir que cada hilo garantiza llamar al método en el Foo desde la misma iteración de bucle en la que se creó el subproceso?

¿O debe copiar la referencia a una nueva variable "local" en cada iteración 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(); 
} 

Actualización: Como se señaló en la respuesta de Jon Skeet, esto no tiene nada que ver específicamente con rosca.

+0

En realidad, creo que tiene que ver con el enhebrado como si no estuvieras usando el enhebrado, llamarías al delegado correcto. En la muestra de Jon Skeet sin enhebrar, el problema es que hay 2 bucles. Aquí hay solo uno, por lo que no debería haber ningún problema ... a menos que no se sepa exactamente cuándo se ejecutará el código (es decir, si usa el enhebrado, la respuesta de Marc Gravell lo muestra perfectamente). – user276648

+0

posible duplicado de [Acceso al cierre modificado (2)] (http://stackoverflow.com/questions/304258/access-to-modified-closure-2) – nawfal

+0

@ user276648 No es necesario enhebrar. Aplazar la ejecución de los delegados hasta después del ciclo sería suficiente para obtener este comportamiento. – binki

Respuesta

97

Editar: todo esto cambia en C# 5, con un cambio en donde se define la variable (a los ojos del compilador). Desde C# 5 en adelante, son lo mismo.


El segundo es seguro; el primero no es.

Con foreach, la variable se declara fuera el bucle - es decir

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

Esto significa que sólo hay 1 f en términos del alcance de cierre, y los hilos podría muy probable que se confunda - pidiendo el método varias veces en algunas instancias y nada en otras. Puedes solucionar este problema con una segunda declaración de variables dentro del bucle:

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

Esto entonces tiene una separada tmp en cada ámbito de cierre, así que no hay riesgo de este problema.

Aquí es una prueba sencilla 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(); 
    } 

salidas (al azar):

1 
3 
4 
4 
5 
7 
7 
8 
9 
9 

agregar una variable temporal y funciona:

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

(cada número una vez, pero por supuesto la orden no está garantizada)

+0

Gracias, Marc. Buen post. – xyz

+0

Holy cow ... esa publicación anterior me salvó un montón de dolor de cabeza. Siempre esperé que la variable foreach fuera delimitada dentro del ciclo. Esa fue una de las principales experiencias de la WTF. – chris

+0

En realidad, se consideró un error en foreach-loop y se corrigió en el compilador. (A diferencia de for-loop donde la variable tiene una sola instancia para todo el ciclo.) –

-5
Foo f2 = f; 

apunta a la misma referencia que

f 

Así que nada pierde y no gana ...

+0

El punto es que el compilador realiza algo de magia aquí para levantar la variable local dentro del alcance del cierre creado implícitamente. La pregunta es si el compilador entiende hacer esto para la variable de ciclo. El compilador es conocido por algunas debilidades con respecto al levantamiento, por lo que esta es una buena pregunta. –

+1

No es magia. Simplemente captura el medio ambiente. El problema aquí y con para bucles, es que la variable de captura se muta (se reasigna). – leppie

+1

leppie: el compilador genera código para usted, y no es fácil ver en general * qué * código es exactamente. Esta es * la * definición de compilación mágica si alguna vez hubo una. –

16

Su necesidad opción 2 para usar, creando un cierre alrededor de una variable cambiante utilizará el valor de la variable cuando se usa la variable y no en el momento de creación del cierre.

The implementation of anonymous methods in C# and its consequences (part 1)

The implementation of anonymous methods in C# and its consequences (part 2)

The implementation of anonymous methods in C# and its consequences (part 3)

Editar: para que quede claro, en C# cierres son "cierres léxicas", que significa que no captan el valor de una variable, pero la propia variable. Eso significa que al crear un cierre a una variable cambiante, el cierre es realmente una referencia a la variable, no una copia de su valor.

Edit2: agregó enlaces a todas las publicaciones del blog si alguien está interesado en leer sobre el compilador interno.

+0

Creo que va por el valor y los tipos de referencia. – leppie

33

Las respuestas de Pop Catalin y Marc Gravell son correctas. Todo lo que quiero agregar es un enlace al my article about closures (que habla sobre Java y C#). Solo pensé que podría agregar un poco de valor.

EDIT: Creo que vale la pena dar un ejemplo que no tiene la imprevisibilidad de enhebrar. Aquí hay un programa corto pero completo que muestra ambos enfoques. La lista de "acción incorrecta" imprime 10 veces; la lista "buena acción" cuenta de 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(); 
     } 
    } 
} 
+1

Gracias - He añadido la pregunta para decir que no se trata realmente de hilos. – xyz

+0

También estaba en una de las charlas que tiene en video en su sitio http://csharpindepth.com/Talks.aspx – rizzle

+0

Sí, parece recordar que usé una versión de subprocesamiento allí, y una de las sugerencias fue: evitar hilos: es más claro usar un ejemplo como el de arriba. –

3

Ésta es una cuestión interesante y parece que hemos visto la gente responde en todas las diversas maneras. Tenía la impresión de que la segunda forma sería la única manera segura. He batido una prueba muy rápida:

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 ejecuta esto, verá que la opción 1 definitivamente no es segura.

+0

Gracias por el programa completo :) – xyz

1

En su caso, puede evitar el problema sin necesidad de utilizar el truco de copia mediante la asignación de su ListOfFoo a una secuencia de hilos:

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