Pregunta

En los dos siguientes fragmentos, es la primera caja de seguridad o debe hacer el segundo?

Por seguro que quiero decir es cada hilo garantizado para llamar al método en el Foo de la misma iteración del bucle en el que se ha creado el hilo?

O debes copiar la referencia a una nueva variable "local" para cada iteración del bucle?

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 ha señalado en la respuesta de Jon Skeet, esto no tiene nada que ver específicamente con rosca

¿Fue útil?

Solución

Editar: todo esto cambia en C # 5, con un cambio en el que se define la variable (a los ojos del compilador). De C # 5 en adelante, que son los mismos .


C # 5 Antes

El segundo es seguro; la primera no lo 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 - Al llamar al método varias veces en algunos casos y no en todos los demás. Puedes solucionar este problema con una segunda declaración de variables dentro de el bucle:

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

Esto entonces tiene una tmp separada en cada alcance de cierre, lo que no hay riesgo de este problema.

Esta 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

Añadir 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 orden no se garantiza)

Otros consejos

Pop Catalin y respuestas de Marc Gravell son correctos. Todo lo que quiero es añadir un enlace a mi artículo sobre el cierre de (que habla tanto de Java y C#). Sólo pensé que podría añadir un poco de valor.

EDIT: Creo que vale la pena dar un ejemplo que no tiene el carácter impredecible de rosca. He aquí un programa corto pero completo que muestra ambos enfoques. La lista "mala acción" imprime 10 diez 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();
        }
    }
}

Su necesidad opción 2, la creación de un cierre alrededor de una variable cambiante utilizará el valor de la variable que se utilizará cuando se utiliza la variable y no en tiempo de creación de cierre.

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

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

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

Editar: para que quede claro, en C # cierres son " cierres léxicos " lo que significa que no captan el valor de una variable, pero la propia variable. Esto significa que cuando se crea un cierre a una variable cambiando el cierre es en realidad una referencia a la variable no una copia de su valor.

Edit2:. Enlaces añadido a todas las entradas de blog si alguien está interesado en leer acerca de partes internas del compilador

Esta es una pregunta interesante y parece que hemos visto la gente responde en todas las diversas maneras. Yo tenía la impresión de que la segunda forma sería la única manera segura. Saqué una verdadera prueba 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 se ejecuta este verá la opción 1 sin duda no es seguro.

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

Ambos son seguros como de C # versión 5 (marco .NET 4.5). Ver esta cuestión para más detalles: tiene el uso de foreach de las variables han cambiado en C # 5?

Foo f2 = f;

puntos a la misma referencia como

f 

Así que nada se pierde y no gana ...

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top