2010-09-09 8 views
11

Lettura this question, volevo testare se potevo dimostrare la non-atomicità di letture e scritture su un tipo per il quale l'atomicità di tali operazioni non è garantita.Perché questo codice non dimostra la non-atomicità delle letture/scritture?

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

Con mia sorpresa, l'affermazione si è rifiutata di fallire anche dopo ben tre minuti di esecuzione. Cosa dà?

  1. Il test non è corretto.
  2. Le specifiche caratteristiche di temporizzazione del test rendono improbabile/impossibile che l'affermazione fallisca.
  3. La probabilità è così bassa che devo eseguire il test per molto più tempo per renderlo probabile che si inneschi.
  4. Il CLR offre maggiori garanzie di atomicità rispetto alle specifiche C#.
  5. Il mio sistema operativo/hardware offre maggiori garanzie rispetto al CLR.
  6. Qualcos'altro?

Naturalmente, non intendo fare affidamento su alcun comportamento che non sia esplicitamente garantito dalle specifiche, ma vorrei una comprensione più approfondita del problema.

FYI, ho fatto questo su entrambi Debug e Release (cambiando Debug.Assert a if(..) throw) profili in due ambienti separati:

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

EDIT: per escludere la possibilità di commento di John Kugelman "il debugger non è Schrödinger-safe" essere il problema, ho aggiunto la linea someList.Add(dCopy); al metodo KeepReading e veri ha dimostrato che questo elenco non vedeva un singolo valore obsoleto dalla cache.

MODIFICA: Sulla base del suggerimento di Dan Bryant: l'utilizzo di long invece di double lo interrompe praticamente all'istante.

+0

La mia ipotesi (ed è proprio così) è la # 3. – AakashM

+0

Controllerei l'IL per assicurarmi che il compilatore non abbia giocato alcun trucco, ma a parte questo non ho ottenuto nulla. Mi aspetto che questo si interrompa su una macchina a 32 bit. – mquander

+1

Aumentare il numero di thread di scrittura: provare a mantenere i thread N + 1 in esecuzione su un sistema N-core. –

risposta

12

Si potrebbe provare a correre attraverso CHESS per vedere se è possibile forzare un interleaving che rompe il test.

Se si dà un'occhiata al diassembly x86 (visibile dal debugger), si potrebbe anche vedere se il jitter sta generando istruzioni che preservano l'atomicità.


MODIFICA: Sono andato avanti e ho eseguito il disassemblaggio (forzando il target x86). Le righe pertinenti sono:

   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] 

Utilizza un solo pst qword fstp per eseguire l'operazione di scrittura in entrambi i casi. La mia ipotesi è che la CPU Intel garantisce l'atomicità di questa operazione, anche se non ho trovato alcuna documentazione per supportare questo. Qualche guru x86 che può confermarlo?


UPDATE:

Questo non riesce come previsto se si utilizza Int64, che utilizza i registri a 32 bit sulla CPU x86, piuttosto che i registri FPU speciali.Si può vedere questo qui sotto:

   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 

UPDATE:

ero curioso di sapere se questo fallirebbe se ho forzato l'allineamento non 8byte del doppio campo nella memoria, così ho messo insieme questo codice:

[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; 
     } 
    } 

e non manca e le istruzioni x86 generate sono essenzialmente gli stessi di prima:

   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] 

Ho sperimentato lo scambio di _d1 e _d2 per l'utilizzo con dCopy/set e ho anche provato un FieldOffset di 2. Tutti hanno generato le stesse istruzioni di base (con offset diversi sopra) e tutti non hanno avuto esito negativo dopo diversi secondi (probabilmente miliardi di tentativi). Sono cautamente fiducioso, dati questi risultati, che almeno le CPU Intel x86 forniscono atomicità di operazioni a doppio carico/immagazzinamento, indipendentemente dall'allineamento.

3

Il compilatore può ottimizzare le letture ripetute di _d. Per quanto ne sa solo analizzare staticamente il tuo loop, _d non cambia mai. Ciò significa che può memorizzare nella cache il valore e non rileggere mai il campo.

Per evitare questo vi sia bisogno di sincronizzare l'accesso a _d (vale a dire lo circondano con una dichiarazione lock), o contrassegnare _d come volatile. Rendendolo volatile dice al compilatore che il suo valore potrebbe cambiare in qualsiasi momento e quindi non dovrebbe mai memorizzare nella cache il valore.

Purtroppo (o per fortuna), non è possibile contrassegnare un campo come doublevolatile, proprio a causa del punto che si sta tentando di testare — double s non è possibile accedere in modo atomico! La sincronizzazione dell'accesso a _d obbliga il compilatore a rileggere il valore, ma anche questo interrompe il test. Oh bene!

+0

Vedo che il metodo 'KeepReading' vede diversi valori di' _d' nel debugger. Inoltre, guardando l'IL, la prima riga * dentro * il ciclo è 'ldsfld float64 Tester.Program :: d', quindi non c'è l'ottimizzazione del compilatore in corso. – Ani

+0

Il debugger non è sicuro da Schrodinger. Stai provando a testare qualcosa di molto basso livello. Questo sta andando un po 'oltre il mio grado di paga, ma ho il sospetto che l'ottimizzatore JIT potrebbe ottimizzare le letture al runtime. È difficile da dire, potrebbe essere anche # 2, # 3 o # 4. –

2

Si potrebbe provare a sbarazzarsi di "dCopy = _d" e utilizzare semplicemente _d nella propria affermazione.

In questo modo due thread stanno leggendo/scrivendo alla stessa variabile allo stesso tempo.

La versione corrente crea una copia del _d che crea una nuova istanza, tutte nello stesso thread, che è un filo funzionamento sicuro:

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

Tutti i membri di questo il tipo è thread-safe. I membri che sembrano modificare lo stato dell'istanza restituiscono effettivamente una nuova istanza inizializzata con il nuovo valore. Come per qualsiasi altro tipo, la lettura e la scrittura di una variabile condivisa che contiene un'istanza di questo tipo deve essere protetta da un blocco per garantire la sicurezza del thread.

Tuttavia, se entrambi i fili sono lettura/scrittura alla stessa istanza variabile quindi:

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

Assegnazione di un'istanza di questo tipo non è thread sicuro su tutte le piattaforme hardware perché la rappresentazione binaria di quell'istanza potrebbe essere troppo grande per essere assegnata in una singola operazione atomica.

Così, se entrambi i fili sono la lettura/scrittura alla stessa istanza variabile si avrebbe bisogno di un blocco per proteggerlo (o Interlocked.Read/Increment/Exchange., Non so se questo funziona su doppie)

Edit

Come sottolineato da others, su reading processore Intel scrittura/a double è un operation atomico. Tuttavia, se il programma è compilato per X86 e utilizza un tipo di dati intero a 64 bit, l'operazione non sarebbe atomica. Come dimostrato nel seguente programma. Sostituire l'Int64 con il doppio e sembra funzionare.

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

Per inciso, potresti provare a utilizzare un costrutto diverso da "Assert". –

+0

In modalità di rilascio, l'ho sostituito con 'if (...) throw'. – Ani

+0

La rimozione di dCopy non fa nulla (eccetto che il thread legge _d due volte anziché una volta). –

0

IMO la risposta corretta è # 5.

doppio è lungo 8 byte.

L'interfaccia di memoria è 64 bit = 8 byte per modulo per orologio (ovvero diventa 16 byte per la memoria a doppio canale).

Ci sono anche cache della CPU. Sulla mia macchina, la linea cache è 64 byte e su tutte le CPU è multiplo di 8.

Come detto dai commenti precedenti, anche quando la CPU è in esecuzione in modalità a 32 bit, le variabili doppie vengono caricate e memorizzate con solo 1 istruzione.

Ecco perché finché la variabile doppia è allineata (sospetto che la macchina virtuale di runtime in linguaggio comune esegua l'allineamento per te), le doppie letture e le scritture siano atomiche.