2010-09-09 10 views
11

Leyendo this question, quería probar si podía demostrar la no atomicidad de lecturas y escrituras en un tipo para el que no se garantiza la atomicidad de tales operaciones.¿Por qué este código no demuestra la no atomicidad de las lecturas/escrituras?

private static double _d; 

[STAThread] 
static void Main() 
{ 
    new Thread(KeepMutating).Start(); 
    KeepReading(); 
} 

private static void KeepReading() 
{ 
    while (true) 
    { 
     double dCopy = _d; 

     // In release: if (...) throw ... 
     Debug.Assert(dCopy == 0D || dCopy == double.MaxValue); // Never fails 
    } 
} 

private static void KeepMutating() 
{ 
    Random rand = new Random(); 
    while (true) 
    { 
     _d = rand.Next(2) == 0 ? 0D : double.MaxValue; 
    } 
} 

Para mi sorpresa, la afirmación se negó a fallar incluso después de tres minutos completos de ejecución. ¿Qué ofrece?

  1. La prueba es incorrecta.
  2. Las características de tiempo específicas de la prueba hacen que sea poco probable/imposible que la afirmación falle.
  3. La probabilidad es tan baja que tengo que ejecutar la prueba durante mucho más tiempo para que sea probable que se active.
  4. El CLR proporciona garantías más sólidas sobre la atomicidad que la especificación de C#.
  5. Mi SO/hardware ofrece garantías más sólidas que el CLR.
  6. ¿Algo más?

Por supuesto, no pretendo confiar en ningún comportamiento que no esté explícitamente garantizado por la especificación, pero me gustaría obtener una comprensión más profunda del problema.

FYI, me encontré con esto en ambos de depuración y de lanzamiento (cambiando Debug.Assert a if(..) throw) perfiles en dos entornos diferentes:

  1. de Windows 7 64-bit + .NET 3.5 SP1
  2. Windows XP 32-bit + .NET 2,0

EDIT: para excluir la posibilidad de que el comentario de John Kugelman "el depurador no es Schrodinger-safe" el principal problema, he añadido la línea someList.Add(dCopy); al método KeepReading y verificación fied que esta lista no estaba viendo un solo valor obsoleto de la memoria caché.

EDITAR: Basado en la sugerencia de Dan Bryant: El uso de long en lugar de double lo rompe de forma casi instantánea.

+0

Mi conjetura (y es mucho más que eso) es # 3. – AakashM

+0

Revisaría el IL para asegurarme de que el compilador no jugara ningún truco, pero aparte de eso no obtuve nada. Esperaría que esto se rompa en una máquina de 32 bits. – mquander

+1

Aumentaría el número de hilos de escritura: trate de mantener N + 1 hilos ejecutándose en un sistema N-core. –

Respuesta

12

Usted puede tratar de ejecución a través de CHESS para ver si se puede forzar una intercalación que rompe la prueba.

Si se echa un vistazo en el desensamblado x 86 (visible desde el depurador), también puede ver si la fluctuación está generando instrucciones que preservan la atomicidad.


EDITAR: Avancé y desensamblé (forzando el objetivo x86). Las líneas relevantes son:

   double dCopy = _d; 
00000039 fld   qword ptr ds:[00511650h] 
0000003f fstp  qword ptr [ebp-40h] 

       _d = rand.Next(2) == 0 ? 0D : double.MaxValue; 
00000054 mov   ecx,dword ptr [ebp-3Ch] 
00000057 mov   edx,2 
0000005c mov   eax,dword ptr [ecx] 
0000005e mov   eax,dword ptr [eax+28h] 
00000061 call  dword ptr [eax+1Ch] 
00000064 mov   dword ptr [ebp-48h],eax 
00000067 cmp   dword ptr [ebp-48h],0 
0000006b je   00000079 
0000006d nop 
0000006e fld   qword ptr ds:[002423D8h] 
00000074 fstp  qword ptr [ebp-50h] 
00000077 jmp   0000007E 
00000079 fldz 
0000007b fstp  qword ptr [ebp-50h] 
0000007e fld   qword ptr [ebp-50h] 
00000081 fstp  qword ptr ds:[00159E78h] 

Utiliza un qst pword de fstp para realizar la operación de escritura en ambos casos. Supongo que la CPU Intel garantiza la atomicidad de esta operación, aunque no he encontrado ninguna documentación que lo respalde. ¿Algún gurú x86 que pueda confirmar esto?


ACTUALIZACIÓN:

Esta falla como se esperaba si se utiliza Int64, que utiliza los registros de 32 bits de la CPU x86 en lugar de los registros especiales de la FPU.Esto se puede ver a continuación:

   Int64 dCopy = _d; 
00000042 mov   eax,dword ptr ds:[001A9E78h] 
00000047 mov   edx,dword ptr ds:[001A9E7Ch] 
0000004d mov   dword ptr [ebp-40h],eax 
00000050 mov   dword ptr [ebp-3Ch],edx 

ACTUALIZACIÓN:

tenía curiosidad si esto fracasaría si Forcé la alineación no 8byte del campo doble en la memoria, así que armar este código:

[StructLayout(LayoutKind.Explicit)] 
    private struct Test 
    { 
     [FieldOffset(0)] 
     public double _d1; 

     [FieldOffset(4)] 
     public double _d2; 
    } 

    private static Test _test; 

    [STAThread] 
    static void Main() 
    { 
     new Thread(KeepMutating).Start(); 
     KeepReading(); 
    } 

    private static void KeepReading() 
    { 
     while (true) 
     { 
      double dummy = _test._d1; 
      double dCopy = _test._d2; 

      // In release: if (...) throw ... 
      Debug.Assert(dCopy == 0D || dCopy == double.MaxValue); // Never fails 
     } 
    } 

    private static void KeepMutating() 
    { 
     Random rand = new Random(); 
     while (true) 
     { 
      _test._d2 = rand.Next(2) == 0 ? 0D : double.MaxValue; 
     } 
    } 

no fallan y las instrucciones x86 generados son esencialmente los mismos que antes:

   double dummy = _test._d1; 
0000003e mov   eax,dword ptr ds:[03A75B20h] 
00000043 fld   qword ptr [eax+4] 
00000046 fstp  qword ptr [ebp-40h] 
       double dCopy = _test._d2; 
00000049 mov   eax,dword ptr ds:[03A75B20h] 
0000004e fld   qword ptr [eax+8] 
00000051 fstp  qword ptr [ebp-48h] 

Experimenté intercambiando _d1 y _d2 por el uso con dCopy/set y también probé un FieldOffset de 2. Todos generaron las mismas instrucciones básicas (con diferentes offsets anteriores) y todos no fallaron después de varios segundos (probablemente miles de millones de intentos) Confío cautelosamente, dados estos resultados, que al menos las CPU Intel x86 proporcionan atomicidad de operaciones de doble carga/almacenamiento, independientemente de la alineación.

3

El compilador puede optimizar las lecturas repetidas de _d. Por lo que sabe simplemente analizando estáticamente su bucle, _d nunca cambia. Esto significa que puede almacenar en caché el valor y nunca volver a leer el campo.

Para evitar esto o hay que hacer sincronizar el acceso a _d (es decir, lo rodean con una declaración lock), o de la marca como _dvolatile. Hacerlo volátil le dice al compilador que su valor podría cambiar en cualquier momento y que nunca debería almacenar en caché el valor.

Desafortunadamente (o afortunadamente), no se puede marcar un campo como doublevolatile, precisamente por el punto que está tratando de probar — double s no se puede acceder de forma atómica! Sincronizar el acceso a _d es lo que obliga al compilador a volver a leer el valor, pero eso también rompe la prueba. ¡Oh bien!

+0

Veo que el método 'KeepReading' ve diferentes valores de' _d' en el depurador. Además, mirando el IL, la primera línea * dentro * del bucle es 'ldsfld float64 Tester.Program :: d', por lo que no hay optimización del compilador. – Ani

+0

El depurador no es seguro para Schrodinger. Intentas probar algo de muy bajo nivel. Esto está un poco más allá de mi grado de pago, pero sospecho que el optimizador de JIT podría optimizar las lecturas en el tiempo de ejecución. Es difícil de decir, podría ser # 2, # 3 o # 4 también. –

2

Puede intentar deshacerse de la 'dCopy = _d' y simplemente usar _d en su afirmación.

De esa manera dos hilos son la lectura/escritura a la misma variable al mismo tiempo.

Su versión actual hace una copia de _D que crea una nueva instancia, todos en el mismo hilo, que es una operación segura hilo:

http://msdn.microsoft.com/en-us/library/system.double.aspx

Todos los miembros de esta tipo son hilos seguros. Los miembros que parecen modificar el estado de la instancia realmente devuelven una nueva instancia inicializada con el nuevo valor. Al igual que con cualquier otro tipo, la lectura y escritura en una variable compartida que contiene una instancia de este tipo debe estar protegida por un bloqueo para garantizar la seguridad del hilo.

Sin embargo, si ambos hilos son la lectura/escritura a la misma instancia de variable a continuación:

http://msdn.microsoft.com/en-us/library/system.double.aspx

Asignación de una instancia de este tipo no es seguro para subprocesos en todas las plataformas de hardware porque la representación binaria de esa instancia puede ser demasiado grande para asignarla en una sola operación atómica.

Así, si ambos hilos son la lectura/escritura a la misma instancia de la variable que se necesita una cerradura para protegerla (o Interlocked.Read/Increment/Exchange., No está seguro si funciona en dobles)

Edición

Como se ha señalado por otros, en una lectura de la CPU Intel/escritura de un doble es una operación atómica. Sin embargo, si el programa está compilado para X86 y utiliza un tipo de datos entero de 64 bits, entonces la operación no sería atómica. Como se demuestra en el siguiente programa. Reemplace el Int64 con doble y parece funcionar.

Public Const ThreadCount As Integer = 2 
    Public thrdsWrite() As Threading.Thread = New Threading.Thread(ThreadCount - 1) {} 
    Public thrdsRead() As Threading.Thread = New Threading.Thread(ThreadCount - 1) {} 
    Public d As Int64 

    <STAThread()> _ 
    Sub Main() 

     For i As Integer = 0 To thrdsWrite.Length - 1 

      thrdsWrite(i) = New Threading.Thread(AddressOf Write) 
      thrdsWrite(i).SetApartmentState(Threading.ApartmentState.STA) 
      thrdsWrite(i).IsBackground = True 
      thrdsWrite(i).Start() 

      thrdsRead(i) = New Threading.Thread(AddressOf Read) 
      thrdsRead(i).SetApartmentState(Threading.ApartmentState.STA) 
      thrdsRead(i).IsBackground = True 
      thrdsRead(i).Start() 

     Next 

     Console.ReadKey() 

    End Sub 

    Public Sub Write() 

     Dim rnd As New Random(DateTime.Now.Millisecond) 
     While True 
      d = If(rnd.Next(2) = 0, 0, Int64.MaxValue) 
     End While 

    End Sub 

    Public Sub Read() 

     While True 
      Dim dc As Int64 = d 
      If (dc <> 0) And (dc <> Int64.MaxValue) Then 
       Console.WriteLine(dc) 
      End If 
     End While 

    End Sub 
+0

Como un lado, puede intentar usar una construcción que no sea 'Assert'. –

+0

En el modo de lanzamiento, lo reemplacé con 'if (...) throw'. – Ani

+0

La eliminación de dCopy no hace nada (excepto hacer que el hilo lea _d dos veces en lugar de una vez). –

0

IMO la respuesta correcta es # 5.

double tiene una longitud de 8 bytes.

La interfaz de memoria es de 64 bits = 8 bytes por módulo por reloj (es decir, se convierte en 16 bytes para la memoria de doble canal).

También hay cachés de CPU. En mi máquina, la línea de caché es de 64 bytes, y en todas las CPU es múltiplo de 8.

Como dicen los comentarios anteriores, incluso cuando la CPU se ejecuta en modo de 32 bits, las variables dobles se cargan y almacenan con solo 1 instrucción

Por eso, siempre que su variable doble esté alineada (sospecho que la máquina virtual de tiempo de ejecución común del lenguaje realiza la alineación), las lecturas dobles y escritas son atómicas.

Cuestiones relacionadas