2009-02-27 7 views
17

Leí en la documentación de MS que la asignación de un valor de 64 bits en una computadora Intel de 32 bits no es una operación atómica; es decir, la operación no es segura para subprocesos. Esto significa que si dos personas asignan simultáneamente un valor a un campo estático Int64, el valor final del campo no se puede predecir.Bajo C# es el uso de Int64 en un procesador de 32 bits peligroso

Tres porciones pregunta:

  • ¿Es esto realmente cierto?
  • ¿Debo preocuparme por esto en el mundo real?
  • Si mi aplicación es multiproceso, ¿realmente necesito rodear todas mis asignaciones Int64 con código de bloqueo?
+3

Para operaciones atómicas en Int64, puede usar la clase InterLocked (http://msdn.microsoft.com/en-us/library/system.threading.interlocked.add.aspx). –

Respuesta

18

No se trata de todas las variables que encuentre. Si alguna variable se usa como un estado compartido o algo así (incluidos, entre otros, algunos camposstatic), debe solucionar este problema. No existe un problema para las variables locales que no se generan como consecuencia de que se las cierra en una transformación de cierre o de iterador y son utilizadas por una sola función (y, por lo tanto, un único hilo) a la vez.

+0

Esto es correcto, sin embargo, podría no estar claro por qué. An Int64 se hereda de System.ValueType, lo que significa que el valor se almacena en la pila. Como cada hilo obtiene su propia pila de llamadas, cada hilo tiene su propio valor, incluso cuando llama a la misma función. – codekaizen

+0

imagina la clase X {int n; }. ¿Es referencia o tipo de valor? ¿Se almacenará en montón o en la pila? –

+0

NS, no creo que esta sea una pregunta relevante, pero las clases son tipos de referencia y se almacenan en un montón. Si mantiene una referencia a una clase en un solo hilo, no tendrá que preocuparse por problemas de bloqueo. –

7

MSDN:

Asignación de una instancia de este tipo es no hilo de seguridad en todo el hardware plataformas porque el binario representación de esa instancia podría ser demasiado grande para asignar en un solo operación atómica.

Pero también:

Al igual que con cualquier otro tipo, la lectura y escritura a una variable compartida que contiene una instancia de este tipo debe estar protegido por una cerradura para garantizar seguridad de los subprocesos.

+2

Es cierto que la palabra clave es ** variable compartida **. –

1

En una plataforma x86 de 32 bits, la mayor parte de la memoria de tamaño atómico es de 32 bits.

Esto significa que si algo escribe o lee de una variable de tamaño de 64 bits es posible que esa lectura/escritura se anule durante la ejecución.

  • Por ejemplo, comienza a asignar un valor a una variable de 64 bits.
  • Después de escribir los primeros 32 bits, el sistema operativo decide que otro proceso obtendrá tiempo de CPU.
  • El próximo proceso intenta leer la variable que estaba en el medio de la asignación.

Es solo una posible condición de carrera con asignación de 64 bits en una plataforma de 32 bits.

Sin embargo, incluso con la variable de 32 bits, puede haber condiciones de carrera con lectura y escritura, por lo que cualquier variable compartida debe sincronizarse de alguna forma para resolver estas condiciones de carrera.

+0

"En una plataforma x86 de 32 bits, la pieza de memoria más grande de tamaño atómico es de 32 bits". - Eso está mal. Puede escribir 8 bytes atómicamente a través de fstp/mmx/sse. –

0

¿Es esto realmente cierto?Sí, como resulta. Si sus registros solo tienen 32 bits en ellos y necesita almacenar un valor de 64 bits en alguna ubicación de memoria, se necesitarán dos operaciones de carga y dos de tienda. Si su proceso se ve interrumpido por otro proceso entre estas dos cargas/tiendas, ¡el otro proceso podría dañar la mitad de sus datos! Extraño pero cierto. Esto ha sido un problema en todos los procesadores que se hayan construido: si su tipo de datos es más largo que sus registros, tendrá problemas de concurrencia.

¿Debo preocuparme por esto en el mundo real? Si y no. Como a casi toda la programación moderna se le asigna su propio espacio de direcciones, solo tendrá que preocuparse por esto si está programando con múltiples subprocesos.

Si mi aplicación es multiproceso, ¿realmente necesito rodear todas mis asignaciones Int64 con código de bloqueo? Tristemente, sí, si quieres ser técnico. En la práctica, es más fácil usar un Mutex o un semáforo alrededor de bloques de código más grandes que para bloquear cada instrucción de conjunto individual en variables accesibles globalmente.

2

Si tiene una variable compartida (por ejemplo, como un campo estático de una clase, o como un campo de un objeto compartido), y ese campo u objeto se va a utilizar de forma cruzada, entonces, sí, usted debe asegurarse de que el acceso a esa variable esté protegido mediante una operación atómica. El procesador x86 tiene intrínsecos para asegurarse de que esto ocurra, y esta instalación se expone a través de los métodos de clase System.Threading.Interlocked.

Por ejemplo:

class Program 
{ 
    public static Int64 UnsafeSharedData; 
    public static Int64 SafeSharedData; 

    static void Main(string[] args) 
    { 
     Action<Int32> unsafeAdd = i => { UnsafeSharedData += i; }; 
     Action<Int32> unsafeSubtract = i => { UnsafeSharedData -= i; }; 
     Action<Int32> safeAdd = i => Interlocked.Add(ref SafeSharedData, i); 
     Action<Int32> safeSubtract = i => Interlocked.Add(ref SafeSharedData, -i); 

     WaitHandle[] waitHandles = new[] { new ManualResetEvent(false), 
              new ManualResetEvent(false), 
              new ManualResetEvent(false), 
              new ManualResetEvent(false)}; 

     Action<Action<Int32>, Object> compute = (a, e) => 
              { 
               for (Int32 i = 1; i <= 1000000; i++) 
               { 
                a(i); 
                Thread.Sleep(0); 
               } 

               ((ManualResetEvent) e).Set(); 
              }; 

     ThreadPool.QueueUserWorkItem(o => compute(unsafeAdd, o), waitHandles[0]); 
     ThreadPool.QueueUserWorkItem(o => compute(unsafeSubtract, o), waitHandles[1]); 
     ThreadPool.QueueUserWorkItem(o => compute(safeAdd, o), waitHandles[2]); 
     ThreadPool.QueueUserWorkItem(o => compute(safeSubtract, o), waitHandles[3]); 

     WaitHandle.WaitAll(waitHandles); 
     Debug.WriteLine("Unsafe: " + UnsafeSharedData); 
     Debug.WriteLine("Safe: " + SafeSharedData); 
    } 
} 

Los resultados:

inseguro: -24,050,275,641 Segura: 0

En una nota interesante, me encontré con este en modo x64 en Vista 64. Esto muestra que los campos de 64 bits son tratados como campos de 32 bits por el tiempo de ejecución, es decir, las operaciones de 64 bits no son atómicas. Alguien sabe si esto es un problema CLR o un problema x64?

+0

Como señalaron Jon Skeet y Ben S, la condición de carrera podría ocurrir entre lecturas y escrituras, por lo que no se puede deducir que las escrituras no fueron atómicas. –

+0

No entiendo ... ese argumento va en cualquier dirección. Por lo que puedo decir, los datos siguen siendo incorrectos. Si ejecuta el ejemplo, es obvio que los datos terminan mal debido a operaciones no atómicas. – codekaizen

+0

El problema no es ni CLR ni x64. Es con tu código. Lo que estás tratando de hacer es lectura atómica + agregar/restar + escribir. Mientras que en x64 tiene garantizada la lectura/escritura atómica de int64. De nuevo, esto es diferente de la lectura atómica + agregar + escribir. –

12

Incluso si las escrituras fueron atómicas, es probable que aún tenga que sacar un candado cada vez que acceda a la variable. Si no hiciste eso, al menos tendrías que hacer la variable volatile para asegurarte de que todos los hilos vieran el nuevo valor la próxima vez que lean la variable (que es casi siempre lo que deseas). Eso te permite hacer conjuntos atómicos y volátiles, pero tan pronto como quieras hacer algo más interesante, como sumarle 5, volverás a bloquearlo.

La programación sin bloqueos es muy, muy difícil de hacer bien. Necesita saber exactamente lo que está haciendo, y mantener la complejidad en un código tan pequeño como sea posible. Personalmente, rara vez trato de intentarlo, excepto para patrones muy conocidos, como usar un inicializador estático para inicializar una colección y luego leer de la colección sin bloquearla.

El uso de la clase Interlocked puede ayudar en algunas situaciones, pero casi siempre es mucho más fácil sacar un candado. Las cerraduras no probadas son "bastante baratas" (sin duda se vuelven caras con más núcleos, pero también lo es todo): no pierdas el tiempo con el código de bloqueo hasta que tengas buenas pruebas de que en realidad marcará una gran diferencia.

Cuestiones relacionadas