2016-01-25 11 views
9

Una classe contiene un attributo che deve essere creato una sola volta. Il processo di creazione è tramite un Func<T> che è argomento di passaggio. Questa è una parte di uno scenario di memorizzazione nella cache.Come testare il codice di test che dovrebbe essere eseguito una sola volta in uno scenario MultiThread?

Il test fa in modo che, indipendentemente da quanti thread tentano di accedere all'elemento, la creazione si verifica una sola volta.

Il meccanismo del test dell'unità è di avviare un numero elevato di thread attorno all'accessor e contare quante volte viene chiamata la funzione di creazione.

Questo non è affatto deterministico, non è garantito che questo stia effettivamente testando un accesso multithread. Forse ci sarà solo un thread alla volta che colpirà il blocco. (In realtà, getFunctionExecuteCount è tra 7 e 9 se il lock non c'è ... Sulla mia macchina, nulla è garantito che sul server CI sarà lo stesso)

Come il test dell'unità può essere riscritto in un modo deterministico? Come essere sicuri che lo lock venga attivato più volte da più thread?

using Microsoft.VisualStudio.TestTools.UnitTesting; 
using System; 
using System.Linq; 
using System.Threading; 
using System.Threading.Tasks; 

namespace Example.Test 
{ 
    public class MyObject<T> where T : class 
    { 
     private readonly object _lock = new object(); 
     private T _value = null; 
     public T Get(Func<T> creator) 
     { 
      if (_value == null) 
      { 
       lock (_lock) 
       { 
        if (_value == null) 
        { 
         _value = creator(); 
        } 
       } 
      } 
      return _value; 
     } 
    } 

    [TestClass] 
    public class UnitTest1 
    { 
     [TestMethod] 
     public void MultipleParallelGetShouldLaunchGetFunctionOnlyOnce() 
     { 
      int getFunctionExecuteCount = 0; 
      var cache = new MyObject<string>(); 

      Func<string> creator =() => 
      { 
       Interlocked.Increment(ref getFunctionExecuteCount); 
       return "Hello World!"; 
      }; 
      // Launch a very big number of thread to be sure 
      Parallel.ForEach(Enumerable.Range(0, 100), _ => 
      { 
       cache.Get(creator); 
      }); 

      Assert.AreEqual(1, getFunctionExecuteCount); 
     } 
    } 
} 

Lo scenario peggiore è se qualcuno rompe il codice lock, e il server di prova ha avuto un certo ritardo. Questo test non deve passare:

using NUnit.Framework; 
using System; 
using System.Linq; 
using System.Threading; 
using System.Threading.Tasks; 

namespace Example.Test 
{ 
    public class MyObject<T> where T : class 
    { 
     private readonly object _lock = new object(); 
     private T _value = null; 
     public T Get(Func<T> creator) 
     { 
      if (_value == null) 
      { 
       // oups, some intern broke the code 
       //lock (_lock) 
       { 
        if (_value == null) 
        { 
         _value = creator(); 
        } 
       } 
      } 
      return _value; 
     } 
    } 

    [TestFixture] 
    public class UnitTest1 
    { 
     [Test] 
     public void MultipleParallelGetShouldLaunchGetFunctionOnlyOnce() 
     { 
      int getFunctionExecuteCount = 0; 
      var cache = new MyObject<string>(); 

      Func<string> creator =() => 
      { 
       Interlocked.Increment(ref getFunctionExecuteCount); 
       return "Hello World!"; 
      }; 
      Parallel.ForEach(Enumerable.Range(0, 2), threadIndex => 
      { 
       // testing server has lag 
       Thread.Sleep(threadIndex * 1000); 
       cache.Get(creator); 
      }); 

      // 1 test passed :'(
      Assert.AreEqual(1, getFunctionExecuteCount); 
     } 
    } 
} 
+0

Se si rende il creatore "lento" (lascia il thread inattivo per un po ') si aumenta la possibilità che altri thread colpiscano il blocco e si debba attendere che il thread lento lasci il blocco. –

+0

Stai provando a testare la sicurezza del thread unitario? Mi sembra strano. Mi sembra che tu testare le funzionalità della classe che esponi, in cui il problema della sicurezza dei thread sarebbe più un argomento per la revisione del codice di questo codice. –

+0

Se qualcuno arriva ed elimina la parte 'lock' del codice, e la revisione del codice non la cattura, non voglio che il test dell'unità lo catturi. –

risposta

5

Per renderlo deterministico, sono necessari solo due thread e si assicura uno dei blocchi all'interno della funzione, mentre l'altro tenta di entrare anche all'interno.

[TestMethod] 
public void MultipleParallelGetShouldLaunchGetFunctionOnlyOnce() 
{ 
    var evt = new ManualResetEvent(false); 

    int functionExecuteCount = 0; 
    var cache = new MyObject<object>(); 

    Func<object> creator =() => 
    { 
     Interlocked.Increment(ref functionExecuteCount); 
     evt.WaitOne(); 
     return new object(); 
    }; 

    var t1 = Task.Run(() => cache.Get(creator)); 
    var t2 = Task.Run(() => cache.Get(creator)); 

    // Wait for one task to get inside the function 
    while (functionExecuteCount == 0) 
     Thread.Yield(); 

    // Allow the function to finish executing 
    evt.Set(); 

    // Wait for completion 
    Task.WaitAll(t1, t2); 

    Assert.AreEqual(1, functionExecuteCount); 
    Assert.AreEqual(t1.Result, t2.Result); 
} 

Si consiglia di impostare un timeout su questo test :)


Ecco una variante che consente di testare più casi:

public void MultipleParallelGetShouldLaunchGetFunctionOnlyOnce() 
{ 
    var evt = new ManualResetEvent(false); 

    int functionExecuteCount = 0; 
    var cache = new MyObject<object>(); 

    Func<object> creator =() => 
    { 
     Interlocked.Increment(ref functionExecuteCount); 
     evt.WaitOne(); 
     return new object(); 
    }; 

    object r1 = null, r2 = null; 

    var t1 = new Thread(() => { r1 = cache.Get(creator); }); 
    t1.Start(); 

    var t2 = new Thread(() => { r2 = cache.Get(creator); }); 
    t2.Start(); 

    // Make sure both threads are blocked 
    while (t1.ThreadState != ThreadState.WaitSleepJoin) 
     Thread.Yield(); 

    while (t2.ThreadState != ThreadState.WaitSleepJoin) 
     Thread.Yield(); 

    // Let them continue 
    evt.Set(); 

    // Wait for completion 
    t1.Join(); 
    t2.Join(); 

    Assert.AreEqual(1, functionExecuteCount); 
    Assert.IsNotNull(r1); 
    Assert.AreEqual(r1, r2); 
} 

Se si vuole ritardare la seconda chiamata , non sarà possibile utilizzare Thread.Sleep, poiché il thread passerà allo stato WaitSleepJoin:

La filettatura è bloccata. Questo potrebbe essere il risultato della chiamata Thread.Sleep o Thread.Join, della richiesta di un blocco, ad esempio chiamando Monitor.Enter o Monitor.Wait oppure di un oggetto di sincronizzazione thread come ManualResetEvent.

E non saremo in grado di dire se il thread sta dormendo o in attesa sul vostro ManualResetEvent ...

Ma si può facilmente sostituire il sonno con un'attesa occupato. Commentare la lock, e cambiare t2 a:

var t2 = new Thread(() => 
{ 
    var sw = Stopwatch.StartNew(); 
    while (sw.ElapsedMilliseconds < 1000) 
     Thread.Yield(); 

    r2 = cache.Get(creator); 
}); 

Ora il test avrà esito negativo.

+1

Ciao @ Lucas, il metodo sembra promettente, ma il test è OK se commento il 'codice di blocco', e avvio il secondo thread dopo un secondo:' var t2 = Task.Factory.StartNew (() => {Thread .Sleep (1000); restituisce cache.Get (creatore);}); ' –

+0

Meraviglioso, questo è esattamente quello che stavo cercando, Manipolazione del thread e Stato del thread. Grazie. –

1

non credo che esista un modo davvero deterministica, ma è possibile aumentare la probabilità in modo che sia molto difficile da non causare gare concomitanti:

Interlocked.Increment(ref getFunctionExecuteCount); 
Thread.Yield(); 
Thread.Sleep(1); 
Thread.Yield(); 
return "Hello World!"; 

By innalzando il parametro Sleep() (a 10?) diventa sempre più improbabile che non avvenga alcuna gara concorrente.

+0

Cosa aggiunge Yield qui, non è sufficiente per dormire? Cosa c'entra il processore con questo? –

+0

L'idea è di scaricare il thread in esecuzione in modo che gli altri thread concorrenti possano essere eseguiti e richiedere il blocco. Yield consente di interrompere l'intervallo di tempo corrente in modo che il round-robin possa scambiare immediatamente un altro intervallo di tempo. – pid

+0

Il sonno farà la stessa cosa E attraverso i processori –

1

In aggiunta alla risposta del Pid:
Questo codice in realtà non crea molti thread.

// Launch a very big number of thread to be sure 
Parallel.ForEach(Enumerable.Range(0, 100), _ => 
{ 
    cache.Get(creator); 
}); 

Avvia i thread ~Environment.ProcessorCount. More details.

Se si desidera ottenere molti thread, è necessario farlo esplicitamente.

var threads = Enumerable.Range(0, 100) 
    .Select(_ => new Thread(() => cache.Get(creator))).ToList(); 
threads.ForEach(thread => thread.Start()); 
threads.ForEach(thread => thread.Join()); 

Quindi se si avranno abbastanza discussioni e le si importerà per passare si otterrà una corsa concorrente.

Se ti interessa il caso in cui il tuo server CI avrà solo un core libero, puoi includere questo vincolo nel test modificando la proprietà Process.ProcessorAffinity. More details.