¿La iteración sobre una matriz con un bucle for es una operación segura para subprocesos en C #? ¿Qué hay de iterar un IEnumerable <T> con un bucle foreach?

8

Según mi entendimiento, dada una matriz C #, el acto de iterar sobre la matriz simultáneamente desde múltiples subprocesos es una operación segura para subprocesos.

Al iterar sobre la matriz, me refiero a leer todas las posiciones dentro de la matriz mediante un bucle antiguo simplefor . Cada subproceso simplemente lee el contenido de una ubicación de memoria dentro de la matriz, nadie escribe nada, por lo que todos los subprocesos leen lo mismo de manera coherente.

Este es un fragmento de código que hace lo que escribí anteriormente:

public class UselessService 
{
   private static readonly string[] Names = new [] { "bob", "alice" };

   public List<int> DoSomethingUseless()
   {
      var temp = new List<int>();

      for (int i = 0; i < Names.Length; i++) 
      {
        temp.Add(Names[i].Length * 2);
      }

      return temp;
   }
}

Entonces, entiendo que el método DoSomethingUseless es seguro para subprocesos y que no hay necesidad de reemplazarlo string[]con un tipo seguro para subprocesos (como, ImmutableArray<string>por ejemplo).

Estoy en lo correcto ?

Ahora supongamos que tenemos una instancia de IEnumerable<T>. No sabemos cuál es el objeto subyacente, solo sabemos que tenemos un objeto implementado IEnumerable<T>, por lo que podemos iterar sobre él utilizando el foreachbucle.

Según mi entendimiento, en este escenario no hay garantía de que iterar sobre este objeto desde varios subprocesos al mismo tiempo sea una operación segura para subprocesos. Dicho de otra manera, es completamente posible que iterar sobre la IEnumerable<T>instancia desde diferentes hilos al mismo tiempo rompa el estado interno del objeto, de modo que se corrompa.

¿Estoy en lo cierto en este punto?

¿Qué pasa con la IEnumerable<T>implementación de la Arrayclase? ¿Es seguro para hilos?

Dicho de otra manera, ¿es seguro el siguiente hilo de código? (este es exactamente el mismo código que el anterior, pero ahora la matriz se repite utilizando un foreachbucle en lugar de un forbucle)

public class UselessService 
{
   private static readonly string[] Names = new [] { "bob", "alice" };

   public List<int> DoSomethingUseless()
   {
      var temp = new List<int>();

      foreach (var name in Names) 
      {
        temp.Add(name.Length * 2);
      }

      return temp;
   }
}

¿Hay alguna referencia que indique qué IEnumerable<T>implementaciones en la biblioteca de clase base .NET son realmente seguras para subprocesos?

Enrico Massone
fuente
44
@Christopher: Re: su primer comentario: en el escenario de Enrico donde solo se lee una matriz , y ni el almacenamiento de la referencia de la matriz ni los elementos de la matriz están mutados, un escenario de múltiples lectores debería ser seguro sin barreras adicionales.
Eric Lippert
3
@Christopher "No, iterar sobre una matriz no es guardar hilos". - Iterar sobre una matriz (es decir Array) será seguro para subprocesos siempre que todos los subprocesos solo lo lean. Lo mismo con List<T>, y probablemente con cualquier otra colección escrita por Microsoft. Pero es posible hacer uno propio IEnumerableque haga cosas que no sean seguras para subprocesos en el enumerador. Por lo tanto, no es una garantía de que todo lo que implemente IEnumerablesea ​​seguro para subprocesos.
Gabriel Luci
66
@Christopher: Re: su segundo comentario: no, foreachfunciona con más que solo enumeradores. El compilador es lo suficientemente inteligente como para saber que si tiene información de tipo estático que indica que la colección es una matriz, omite generar el enumerador y solo accede a la matriz directamente como si fuera un forbucle. Del mismo modo, si la colección admite una implementación personalizada de GetEnumerator, esa implementación se usa en lugar de llamar IEnumerable.GetEnumerator. La noción de que foreachsolo se trafica IEnumerable/IEnumeratores falsa.
Eric Lippert
3
@ Christopher: Re: su tercer comentario: tenga cuidado. Aunque puede haber algo así como "rancio", la noción de que, por lo tanto, también existe algo "fresco" no es exacta. volatileno garantiza que obtenga la actualización más reciente posible a una variable porque C # permite que diferentes hilos observen diferentes ordenamientos de lecturas y escrituras, y por lo tanto la noción de "más reciente" no está definida globalmente . En lugar de razonar sobre la frescura, razone sobre las restricciones que volatile impone la forma en que se pueden reordenar las escrituras.
Eric Lippert
66
@EnricoMassone: No estoy tratando de responder ninguna de sus preguntas, pero sí noto que sus preguntas indican que desea hacer algo muy arriesgado, a saber, escribir una solución de bloqueo bajo o sin bloqueo que comparta la memoria entre hilos. No intentaría hacerlo, excepto en los casos en que realmente no había otra opción que cumpliera con mis objetivos de rendimiento, y en los casos en los que me sentí seguro de analizar a fondo y con precisión todos los posibles reordenamientos de todas las lecturas y escrituras en el programa . Dado que hace estas preguntas, creo que se siente menos seguro de lo que me sentiría cómodo.
Eric Lippert

Respuestas:

2

¿La iteración sobre una matriz con un bucle for es una operación segura para subprocesos en C #?

Si está estrictamente hablando lectura desde varios subprocesos , que sea seguro para el hilo Arrayy List<T>y casi todas las colecciones escrito por Microsoft, independientemente de si está utilizando una foro foreachbucle. Especialmente en el ejemplo que tienes:

var temp = new List<int>();

foreach (var name in Names)
{
  temp.Add(name.Length * 2);
}

Puede hacerlo a través de tantos hilos como desee. Todos leerán los mismos valores Namesfelizmente.

Si le escribe desde otro hilo (esta no era su pregunta, pero vale la pena señalar)

Iterando sobre un Arrayo List<T>con un forbucle, seguirá leyendo y felizmente leerá los valores modificados a medida que los encuentre.

Iterando con un foreachbucle, entonces depende de la implementación. Si un valor en una Arrayparte cambia a través de un foreachbucle, simplemente seguirá enumerando y le dará los valores cambiados.

Con List<T>, depende de lo que consideres "seguro para subprocesos". Si está más interesado en leer datos precisos, entonces es "seguro", ya que arrojará una excepción a mitad de la enumeración y le dirá que la colección cambió. Pero si considera que lanzar una excepción no es seguro, entonces no es seguro.

Pero vale la pena señalar que esta es una decisión de diseño List<T>, hay un código que busca cambios explícitamente y lanza una excepción. Las decisiones de diseño nos llevan al siguiente punto:

¿Podemos suponer que cada colección que implementa IEnumerablees segura para leer en múltiples hilos?

En la mayoría de los casos lo será, pero no se garantiza una lectura segura para subprocesos. La razón es porque todo IEnumerablerequiere una implementación de IEnumerator, que decide cómo atravesar los elementos de la colección. Y al igual que cualquier clase, puede hacer lo que quiera allí, incluidas cosas que no son seguras para subprocesos como:

  • Usando variables estáticas
  • Usar un caché compartido para leer valores
  • No hacer ningún esfuerzo para manejar casos donde la colección cambia a mitad de la enumeración
  • etc.

Incluso podría hacer algo extraño como GetEnumerator()devolver la misma instancia de su enumerador cada vez que se llama. Eso realmente podría generar algunos resultados impredecibles.

Considero que algo no es seguro para subprocesos si puede dar lugar a resultados impredecibles. Cualquiera de esas cosas podría causar resultados impredecibles.

Puede ver el código fuente para el Enumeratorque List<T>usa , por lo que puede ver que no hace nada de esas cosas raras, lo que le dice que enumerar List<T>desde múltiples hilos es seguro.

Gabriel Luci
fuente
básicamente, si alguien le da una instancia de IEnumerable y lo único que sabe es que implementa IEnumerable, entonces no es seguro enumerar ese objeto desde diferentes hilos. De lo contrario, si tiene una instancia de una clase BCL, digamos una instancia de List <string>, puede enumerar de forma segura el objeto de varios subprocesos (incluso si el tipo List <string> en sí mismo no es seguro para subprocesos en todos sus miembros, usted puede estar seguro de que su implementación de la interfaz IEnumerable es segura para subprocesos).
Enrico Massone
1
toda la discusión sobre la seguridad de las enumeraciones concurrentes de colecciones BCL se basa en una suposición: podemos leer el código fuente proporcionado por microsoft y, a partir de hoy, no conocemos ningún ejemplo de clase BCL que no sea segura para subprocesos con respecto a concurrentes enumeración. Básicamente confiamos en un detalle de implementación. A menos que algo no esté explícitamente documentado, suponer que no es la opción más segura. Por lo tanto, la opción más segura es probablemente evitar problemas y usar algo como un ImmutableArray <cadena>, que sabemos con certeza que es seguro para subprocesos debido a su inmutabilidad.
Enrico Massone
1
"la opción más segura es probablemente evitar problemas y usar algo como un ImmutableArray <cadena>" : si tiene control sobre el tipo de colección y sabe que solo leerá a través de subprocesos, entonces List<T>o Arrayes tan seguro como ImmutableArray<string>, ya que podemos probar que son seguros para leer en subprocesos. Solo buscaría otras opciones si planea leer y escribir en varios hilos. Podrías usar una colección apropiada de System.Collections.Concurrent.
Gabriel Luci
1

Afirmar que su código es seguro para subprocesos significa que debemos dar por sentado sus palabras de que no hay ningún código dentro UselessServiceque intente reemplazar simultáneamente el contenido de la Namesmatriz con algo como "tom" and "jerry"o (más siniestro) null and null. Por otra parte el uso de un ImmutableArray<string>le garantiza que el código es seguro para subprocesos, y todo el mundo podía estar seguro de eso con sólo mirar el tipo del campo de sólo lectura estática, sin tener que inspeccionar cuidadosamente el resto del código.

Puede encontrar interesantes estos comentarios del código fuente de la ImmutableArray<T>, con respecto a algunos detalles de implementación de esta estructura:

Una matriz de solo lectura con O (1) tiempo de búsqueda indexable.

Este tipo tiene un contrato documentado de ser exactamente un tamaño de campo de tipo de referencia. Nuestra propia System.Collections.Immutable.ImmutableInterlockedclase depende de ello, así como de otros externamente.

AVISO IMPORTANTE PARA MANTENEDORES Y REVISORES:

Este tipo debe ser seguro para subprocesos. Como estructura, no puede proteger sus propios campos del cambio de un hilo mientras sus miembros se ejecutan en otros hilos porque las estructuras pueden cambiar en su lugar simplemente reasignando el campo que contiene esta estructura. Por lo tanto, es extremadamente importante que cada miembro solo desreferencia una vez this. Si un miembro necesita hacer referencia al campo de matriz, eso cuenta como una desreferencia de this. Llamar a otros miembros de la instancia (propiedades o métodos) también cuenta como desreferenciación this. Cualquier miembro que necesite usar thismás de una vez debe asignarthisa una variable local y usar eso para el resto del código en su lugar. Esto efectivamente copia un campo en la estructura a una variable local para que esté aislado de otros hilos.

Theodor Zoulias
fuente