2009-09-04 9 views
25

Tengo el siguiente fragmento de código:¿El retorno de rendimiento en C# es seguro para subprocesos?

private Dictionary<object, object> items = new Dictionary<object, object>; 
public IEnumerable<object> Keys 
{ 
    get 
    { 
     foreach (object key in items.Keys) 
     { 
      yield return key; 
     } 
    } 
} 

Es esto seguro para subprocesos? Si no, ¿tengo que poner un lock alrededor del lazo o el yield return?

Esto es lo que quiero decir:

Thread1 acceso a la propiedad Keys mientras Thread2 añade un elemento al diccionario subyacente. ¿Thread1 está afectado por el agregado de Thread2?

+0

Su segundo exemple se bloqueará, devolver un enumerable desbloquee. Y repetirá DESPUÉS del desbloqueo. – Guillaume

+0

Oh, tienes razón. Me acabo de dar cuenta de que es un mal ejemplo. Editaré la pregunta. – Albic

+0

Solo para arrojar algo de luz: la seguridad del hilo a través del bloqueo es costosa, por lo que no tiene sentido bloquear automáticamente cada acción a menos que usted lo solicite explícitamente. –

Respuesta

7

Bien, hice algunas pruebas y obtuve un resultado interesante.

Parece que es más un problema del enumerador de la colección subyacente que la palabra clave yield. El enumerador (en realidad su método MoveNext) lanza (si se implementó correctamente) un InvalidOperationException porque la enumeración ha cambiado. De acuerdo con el MSDN documentation of the MoveNext method este es el comportamiento esperado.

Debido a que enumerar a través de una colección generalmente no es seguro para subprocesos tampoco es yield return.

20

¿Qué quieres decir con thread-safe?

Sin duda no debería cambiar el diccionario mientras está iterando sobre él, ya sea en el mismo hilo o no.

Si se está accediendo al diccionario en varios subprocesos en general, la persona que llama debe obtener un bloqueo (la misma que cubre todos los accesos) para que puedan bloquear durante la duración de la iteración en el resultado.

EDITAR: para responder a su edición, no en de ninguna manera corresponde al código de bloqueo. No hay ningún bloqueo que se elimine automáticamente por un bloque de iterador, y ¿cómo podría saber acerca de syncRoot de todos modos?

Además, así el bloqueo del retorno de la IEnumerable<TKey> no significa que sea seguro para subprocesos, ya sea - porque el bloqueo sólo afecta el periodo de tiempo cuando es regresar la secuencia, y no el período durante el cual se está repiten a lo largo.

+0

Sí, quise decir Diccionario en lugar de Lista. La primera edición fue incorrecta (lo siento) y la eliminé. Agregué el comportamiento esperado a la pregunta. – Albic

18

Salida este post sobre lo que sucede detrás de las escenas con la palabra clave yield:

Behind the scenes of the C# yield keyword

En pocas palabras - el compilador toma la palabra clave de rendimiento y genera una clase entera de la IL para soportar la funcionalidad. Puede consultar la página después del salto y ver el código que se genera ... y ese código parece que rastrea la identificación del hilo para mantener las cosas seguras.

+0

+1 para el enlace realmente útil. – Albic

3

Creo que sí, pero no puedo encontrar una referencia que lo confirme. Cada vez que un subproceso llama foreach en un iterador, una nueva secuencia de procesamiento local * instancia de la IEnumerator subyacente debe conseguir creado, por lo que no debería haber ningún estado de la memoria "compartida" que dos hilos pueden entrar en conflicto sobre ...

  • Thread Local: en el sentido de que su variable de referencia tiene un alcance en un marco de pila de métodos en esa hebra
+0

Sí, pero ¿qué ocurre si llamo a GetEnumerator y luego comparto el IEnumerator? Debería aclarar qué está haciendo con sus hilos. – Guillaume

3

Creo que la implementación de rendimiento es segura para subprocesos. De hecho, puede ejecutar ese programa simple en su casa y notará que el estado del método listInt() se guarda y restaura correctamente para cada subproceso sin efecto de borde de otros subprocesos.

public class Test 
{ 
    public void Display(int index) 
    { 
     foreach (int i in listInt()) 
     { 
      Console.WriteLine("Thread {0} says: {1}", index, i); 
      Thread.Sleep(1); 
     } 

    } 

    public IEnumerable<int> listInt() 
    { 
     for (int i = 0; i < 5; i++) 
     { 
      yield return i; 
     } 
    } 
} 

class MainApp 
{ 
    static void Main() 
    { 
     Test test = new Test(); 
     for (int i = 0; i < 4; i++) 
     { 
      int x = i; 
      Thread t = new Thread(p => { test.Display(x); }); 
      t.Start(); 
     } 

     // Wait for user 
     Console.ReadKey(); 
    } 
} 
+0

+1. Yo también solo verifiqué que las máquinas de estado de iterador de rendimiento generadas por el compilador C# 4.0 son seguras para hilos. –

2
class Program 
{ 
    static SomeCollection _sc = new SomeCollection(); 

    static void Main(string[] args) 
    { 
     // Create one thread that adds entries and 
     // one thread that reads them 
     Thread t1 = new Thread(AddEntries); 
     Thread t2 = new Thread(EnumEntries); 

     t2.Start(_sc); 
     t1.Start(_sc); 
    } 

    static void AddEntries(object state) 
    { 
     SomeCollection sc = (SomeCollection)state; 

     for (int x = 0; x < 20; x++) 
     { 
      Trace.WriteLine("adding"); 
      sc.Add(x); 
      Trace.WriteLine("added"); 
      Thread.Sleep(x * 3); 
     } 
    } 

    static void EnumEntries(object state) 
    { 
     SomeCollection sc = (SomeCollection)state; 
     for (int x = 0; x < 10; x++) 
     { 
      Trace.WriteLine("Loop" + x); 
      foreach (int item in sc.AllValues) 
      { 
       Trace.Write(item + " "); 
      } 
      Thread.Sleep(30); 
      Trace.WriteLine(""); 
     } 
    } 
} 

class SomeCollection 
{ 
    private List<int> _collection = new List<int>(); 
    private object _sync = new object(); 

    public void Add(int i) 
    { 
     lock(_sync) 
     { 
      _collection.Add(i); 
     } 
    } 


    public IEnumerable<int> AllValues 
    { 
     get 
     { 
      lock (_sync) 
      { 
       foreach (int i in _collection) 
       { 
        yield return i; 
       } 
      } 
     } 
    } 
} 
+0

Aquí hay un ejemplo simple que muestra que el rendimiento NO es seguro para hilos por sí mismo. Como está escrito, es seguro para subprocesos, pero si comenta el bloqueo (_sync) en AllValues, debería poder verificar que no es seguro para subprocesos ejecutándolo unas pocas veces. Si obtienes una InvalidOperationException, prueba que NO es segura para subprocesos. – sjp

Cuestiones relacionadas