En los dos siguientes fragmentos, ¿el primero es seguro o debe hacer el segundo?
Por seguro, quiero decir, ¿se garantiza que cada hilo llame al método en Foo desde la misma iteración de bucle en el 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<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 el enhebrado.
c#
enumeration
closures
xyz
fuente
fuente
Respuestas:
Editar: todo esto cambia en C # 5, con un cambio en el lugar donde se define la variable (a los ojos del compilador). Desde C # 5 en adelante, son iguales .
Antes de C # 5
El segundo es seguro; el primero no lo es.
Con
foreach
, la variable se declara fuera del ciclo, es decirFoo f; while(iterator.MoveNext()) { f = iterator.Current; // do something with f }
Esto significa que solo hay 1
f
en términos del alcance de cierre, y es muy probable que los subprocesos se confundan, llamando al método varias veces en algunas instancias y no en absoluto en otras. Puede solucionar esto con una segunda declaración de variable dentro del ciclo:foreach(Foo f in ...) { Foo tmp = f; // do something with tmp }
Esto luego tiene un
tmp
alcance separado en cada 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 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 el pedido no está garantizado)
fuente
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 de Java y C #). Solo pensé que podría agregar un poco de valor.
EDITAR: Creo que vale la pena dar un ejemplo que no tenga la imprevisibilidad del subproceso. Aquí hay un programa breve pero completo que muestra ambos enfoques. La lista de "malas acciones" se imprime 10 diez veces; la lista de "buenas acciones" 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(); } } }
fuente
for
ciclo, este comportamiento me confunde. Por ejemplo, en su ejemplo de comportamiento de cierre, stackoverflow.com/a/428624/20774 , la variable existe fuera del cierre pero se une correctamente. ¿Por qué es esto diferente?Su necesidad de usar la opción 2, crear un cierre alrededor de una variable cambiante utilizará el valor de la variable cuando se utilice la variable y no en el momento de la 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 que quede claro, en C # los cierres son " cierres léxicos ", lo que significa que no capturan el valor de una variable, sino la variable en sí. Eso significa que cuando se crea un cierre para una variable cambiante, el cierre es en realidad una referencia a la variable, no una copia de su valor.
Edit2: se agregaron enlaces a todas las publicaciones del blog si alguien está interesado en leer sobre los componentes internos del compilador.
fuente
Esta es una pregunta interesante y parece que hemos visto a personas responder de diversas maneras. Tenía la impresión de que el segundo camino sería el único seguro. Azoté una prueba realmente 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.
fuente
En su caso, puede evitar el problema sin utilizar el truco de copiar asignando su
ListOfFoo
a una secuencia de subprocesos:var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething())); foreach (var t in threads) { t.Start(); }
fuente
Ambos son seguros a partir de la versión 5 de C # (.NET framework 4.5). Consulte esta pregunta para obtener más detalles: ¿Se ha cambiado el uso de variables de foreach en C # 5?
fuente
apunta a la misma referencia que
Así que nada perdido y nada ganado ...
fuente