El identificador de foreach y cierres

En los dos fragmentos siguientes, ¿es seguro el primero o debe hacer el segundo?

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 hilo.

¿O debe copiar la referencia a una nueva variable “local” en cada iteración del ciclo?

var threads = new List(); foreach (Foo f in ListOfFoo) { Thread thread = new Thread(() => f.DoSomething()); threads.Add(thread); thread.Start(); } 

 var threads = new List(); foreach (Foo f in ListOfFoo) { Foo f2 = f; Thread thread = new Thread(() => f2.DoSomething()); threads.Add(thread); thread.Start(); } 

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

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


El segundo es seguro; el primero no es.

Con foreach , la variable se declara fuera del ciclo, es decir,

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

Esto significa que solo hay 1 f en términos del scope del cierre, y es muy probable que los hilos se confundan, llamando al método varias veces en algunas instancias y nada en otras. Puedes arreglar esto con una segunda statement de variable dentro del ciclo:

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

Esto tiene un tmp separado en cada ámbito de cierre, por lo que no hay riesgo de este problema.

Aquí hay una prueba simple 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 

Agregue una variable temp 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)

Las respuestas de Pop Catalin y Marc Gravell son correctas. Todo lo que quiero agregar es un enlace a mi artículo sobre cierres (que habla sobre Java y C #). Solo pensé que podría agregar un poco de valor.

EDITAR: Creo que vale la pena dar un ejemplo que no tiene la imprevisibilidad de enhebrar. Aquí hay un progtwig corto pero completo que muestra ambos enfoques. La lista de “acción incorrecta” imprime 10 veces; la lista de “buenas acciones” cuenta de 0 a 9.

 using System; using System.Collections.Generic; class Test { static void Main() { List badActions = new List(); List goodActions = new List(); 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(); } } } 

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

La implementación de métodos anónimos en C # y sus consecuencias (parte 1)

La implementación de métodos anónimos en C # y sus consecuencias (parte 2)

La implementación de métodos anónimos en C # y sus consecuencias (parte 3)

Editar: para dejarlo en claro, en C # los cierres son ” cierres léxicos ” lo que significa que no capturan el valor de una variable sino la variable misma. 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: enlaces agregados a todas las publicaciones del blog si alguien está interesado en leer sobre el comstackdor interno.

Esta es una pregunta interesante y parece que hemos visto a personas responder de varias maneras. Tenía la impresión de que la segunda forma sería la única manera segura. Lancé 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(); ListOfFoo.Add(new Foo(1)); ListOfFoo.Add(new Foo(2)); ListOfFoo.Add(new Foo(3)); ListOfFoo.Add(new Foo(4)); var threads = new List(); 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.

En su caso, puede evitar el problema sin utilizar el truco de copia asignando su ListOfFoo a una secuencia de hilos:

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

Ambos son seguros desde C # versión 5 (.NET framework 4.5). Vea esta pregunta para más detalles: ¿Se ha cambiado el uso de las variables por parte de Foreach en C # 5?

 Foo f2 = f; 

apunta a la misma referencia que

 f 

Entonces nada perdido y nada ganado …