2013-07-01 7 views
5

Sto scrivendo unit test per AtomicInteger e AtomicBoolean. Saranno utilizzati come test di riferimento per testare emulazioni di queste classi in ogg-c, per l'uso in progetti tradotti.Come testare l'atomicità AtomicBoolean?

Il test AtomicInteger funzionava bene Credo, fondamentalmente eseguendo un numero prevedibile di incremento, decremento, aggiungere e sottrarre operazioni in un gran numero di cicli for, ognuno dei quali esegue nel proprio filo (e molti fili per tipo di operazione) . Le operazioni effettive iniziano simultaneamente con CountDownLatch.

Dopo aver eseguito tutti i thread, si asserisce confrontando il numero intero atomico con il valore intero previsto in base al numero di thread, alle iterazioni per thread e all'aumento/diminuzione previsto per iterazione. Questo test passa.

Ma come testare AtomicBoolean? Le operazioni di base sono get e set in modo tale che chiamare molte volte in molti thread e aspettarsi che il risultato finale sia vero o falso non sembra avere senso. La direzione che sto pensando è di usare due AtomicBooleans che dovrebbero avere sempre valori opposti. In questo modo:

@Test 
public void testAtomicity() throws Exception { 

    // ==== SETUP ==== 
    final AtomicBoolean booleanA = new AtomicBoolean(true); 
    final AtomicBoolean booleanB = new AtomicBoolean(false); 

    final int threadCount = 50; 

    final int iterationsPerThread = 5000; 

    final CountDownLatch startSignalLatch = new CountDownLatch(1); 
    final CountDownLatch threadsFinishedLatch = new CountDownLatch(threadCount); 

    final AtomicBoolean assertFailed = new AtomicBoolean(false); 

    // ==== EXECUTE: start all threads ==== 
    for (int i = 0; i < threadCount; i++) { 

     // ==== Create the thread ===== 
     AtomicOperationsThread thread; 
     thread = new AtomicOperationsThread("Thread #" + i, booleanA, booleanB, startSignalLatch, threadsFinishedLatch, iterationsPerThread, assertFailed); 
     System.out.println("Creating Thread #" + i); 

     // ==== Start the thread (each thread will wait until the startSignalLatch is triggered) ===== 
     thread.start(); 
    } 

    startSignalLatch.countDown(); 

    // ==== VERIFY: that the AtomicInteger has the expected value after all threads have finished ==== 
    final boolean allThreadsFinished; 
    allThreadsFinished = threadsFinishedLatch.await(60, TimeUnit.SECONDS); 

    assertTrue("Not all threads have finished before reaching the timeout", allThreadsFinished); 
    assertFalse(assertFailed.get()); 

} 

private static class AtomicOperationsThread extends Thread { 

    // ##### Instance variables ##### 

    private final CountDownLatch startSignalLatch; 
    private final CountDownLatch threadsFinishedLatch; 

    private final int iterations; 

    private final AtomicBoolean booleanA, booleanB; 

    private final AtomicBoolean assertFailed; 

    // ##### Constructor ##### 

    private AtomicOperationsThread(final String name, final AtomicBoolean booleanA, final AtomicBoolean booleanB, final CountDownLatch startSignalLatch, final CountDownLatch threadsFinishedLatch, final int iterations, final AtomicBoolean assertFailed) { 

     super(name); 
     this.booleanA = booleanA; 
     this.booleanB = booleanB; 
     this.startSignalLatch = startSignalLatch; 
     this.threadsFinishedLatch = threadsFinishedLatch; 
     this.iterations = iterations; 
     this.assertFailed = assertFailed; 
    } 

    // ##### Thread implementation ##### 

    @Override 
    public void run() { 

     super.run(); 

     // ==== Wait for the signal to start (so all threads are executed simultaneously) ===== 
     try { 
      System.out.println(this.getName() + " has started. Awaiting startSignal."); 
      startSignalLatch.await(); /* Awaiting start signal */ 
     } catch (InterruptedException e) { 
      throw new RuntimeException("The startSignalLatch got interrupted.", e); 
     } 

     // ==== Perform the atomic operations ===== 
     for (int i = 0; i < iterations; i++) { 

      final boolean booleanAChanged; 
      booleanAChanged = booleanA.compareAndSet(!booleanB.get(), booleanB.getAndSet(booleanA.get())); /* Set A to the current value of B if A is currently the opposite of B, then set B to the current value of A */ 

      if (!booleanAChanged){ 
       assertFailed.set(true); 
       System.out.println("Assert failed in thread: " + this.getName()); 
      } 
     } 

     // ==== Mark this thread as finished ===== 
     threadsFinishedLatch.countDown(); 
    } 
} 

Questo funziona con un thread ma non riesce con più. Immagino che questo sia dovuto al fatto che booleanAChanged = booleanA.compareAndSet(!booleanB.get(), booleanB.getAndSet(booleanA.get())); non è un'operazione atomica.

Qualche suggerimento?

+1

Sono sicuro che hai - ma assicuratevi di pensare a tutto quando si verifica questo tipo di basso livello di concorrenza vale a dire l'hardware è in esecuzione, il sistema operativo, la JVM - si può tutte le cose ad effetto come questo e significa che i test che passano sempre in un contesto falliscono in un altro. – selig

+0

Grazie per il tuo feedback. Presumo che tu ti stia riferendo al test AtomicInteger, giusto? Mi piacerebbe essere sicuro che non ci siano falsi positivi; Puoi indicare possibili miglioramenti per questo approccio e/o dare esempi di come qualcosa può andare storto? –

risposta

4

Mi concentrerei su compareAndSet, che è la vera differenza tra un AtomicBoolean e un ordinario boolean.

Ad esempio, utilizzare compareAndSet(false, true) per controllare un'area critica. Fai il loop fino a farlo tornare falso, quindi inserisci la regione critica. Nella regione critica, fare qualcosa che è molto probabile che fallisca se due o più thread lo eseguono contemporaneamente. Ad esempio, incrementa un contatore con un breve sonno tra la lettura del vecchio valore e la scrittura del nuovo valore. Alla fine dell'area critica, impostare AtomicBoolean su falso.

Inizializzare lo AtomicBoolean su falso e su zero prima di iniziare i thread.

for(int i=0; i<iterations; i++) { 
    while (!AtomicBooleanTest.atomic.compareAndSet(false, true)); 
    int oldValue = AtomicBooleanTest.globalCounter; 
    Thread.sleep(1); 
    AtomicBooleanTest.globalCounter = oldValue + 1; 
    AtomicBooleanTest.atomic.set(false); 
} 

Alla fine, il valore globalCounter dovrebbe essere t*iterations dove t è il numero di fili.

Il numero di thread deve essere simile al numero che l'hardware può eseguire simultaneamente e molto più probabilmente fallire su un multiprocessore che su un singolo processore. Il più alto rischio di fallimento è immediatamente dopo che AtomicBoolean diventa falso. Tutti i processori disponibili dovrebbero cercare simultaneamente di ottenere un accesso esclusivo ad esso, vederlo essere falso e cambiarlo atomicamente in vero.

+0

Approccio interessante. Mi c'è voluto un po 'per afferrarla, ma passa ogni volta è come questo (in ogni thread, con 500 thread): (! SUT.compareAndSet (false, true)) 1. 'po';' 2. ' int oldValue = AtomicBooleanTest.globalCounter; ' 3.' Thread.sleep (1); ' 4.' SUT.set (false); ' Al termine di tutti i thread, globalCounter dovrebbe essere 500. Presumo che questo sia ciò che avevo in mente, giusto? –

+2

@RolfW. Ho incorporato il tuo codice nella risposta, formattato e leggermente modificato. Anche se potrebbe valere la pena sperimentare, sarei sorpreso se fosse utile avere molti più thread di quanto l'hardware possa funzionare in parallelo. –

+0

Questo ha senso, penso che tu abbia ragione. Dal momento che la logica del test è interamente legata alla CPU, il computer probabilmente perderebbe tempo a gestire i thread che potrebbe anche utilizzare cercando di entrare in conflitto. Ma mi sono reso conto che Thread.sleep() potrebbe essere anche un fattore: mi aspetterei che lo stato di riposo sia tempo di inattività che potrebbe essere utilizzato aggiungendo più thread, giusto? Detto questo, ho fatto un rapido test; Quando corro usando esattamente il numero di core disponibili, utilizza quasi il 100% - circa lo stesso quando si usano 16 o 500 thread. Ciò suggerisce che il tempo di sonno è trascurabile. Come funziona? –

1

Si tratta di quattro operazioni atomiche. Dato che vuoi solo che un booleano sia l'opposto dell'altro, basta avere un booleano e continuare a farlo. È possibile calcolare l'altro da questo valore.

3

Penso che sarà più difficile testarlo rispetto a uno AtomicInteger, come si fa notare. Lo spazio dei valori possibili è molto più piccolo, e quindi lo spazio delle cose possibili-che-può-sbagliare è molto più piccolo. Poiché test come questo fondamentalmente si riducono alla fortuna (con un sacco di loop per aumentare le tue possibilità), sarà più difficile colpire quell'obiettivo più piccolo.

Il mio consiglio sarebbe quello di lanciare un sacco di discussioni che hanno accesso a una singola AtomicBoolean. Ognuno di essi esegue un CAS e solo in caso di esito positivo, incrementa atomicamente uno AtomicInteger. Quando tutti i thread sono terminati, dovresti vedere solo un singolo incremento per quello AtomicInteger. Quindi basta sciacquare, schiumare, ripetere.

+0

Grazie. Questo test passa ogni volta, anche quando si esegue il test in un loop 1.000 volte utilizzando 1.500 thread. Ecco il codice che ho usato: 'booleano finale valueUpdated;' ' valueUpdated = SUT.compareAndSet (true, false);' ' se (valueUpdated) valueUpdatedCount.incrementAndGet();' Posso essere sicuro che questo non è un falso positivo? –

+0

@RolfW. - Sfortunatamente no. –

+0

@Stephen Attualmente ho implementato sia la soluzione di yshavit che quella proposta da Patricia Shanahan con molti thread e iterazioni. AtomicBoolean sarà utilizzato in normali app mobili e web, niente di scientifico o di troppo matematico. Dopo averlo testato in questo modo, diresti che esiste una probabilità abbastanza significativa di bug in app del genere? Se improbabile, mi accontento di questo. Altrimenti, potresti spiegare un po 'di più sulla tua soluzione? –