2009-04-26 3 views
8

Sto scrivendo una classe che so che ha bisogno di un blocco, perché la classe deve essere thread-safe. Ma siccome sono Test-Driven-Developing so che non posso scrivere una riga di codice prima di creare un test per questo. E trovo molto difficile da fare poiché i test diventeranno molto complessi alla fine. Che cosa fai di solito in questi casi? C'è qualche strumento per aiutarti?Devo eseguire un test dell'unità per problemi di multithreading prima di scrivere qualsiasi blocco?

Questa domanda è NET specifica

Qualcuno ha chiesto il codice:

public class StackQueue 
{ 
    private Stack<WebRequestInfo> stack = new Stack<WebRequestInfo>(); 
    private Queue<WebRequestInfo> queue = new Queue<WebRequestInfo>(); 

    public int Count 
    { 
     get 
     { 
      return this.queue.Count + this.stack.Count; 
     } 
    } 

    public void Enqueue(WebRequestInfo requestInfo) 
    { 
     this.queue.Enqueue(requestInfo); 
    } 

    public void Push(WebRequestInfo requestInfo) 
    { 
     this.stack.Push(requestInfo); 
    } 

    private WebRequestInfo Next() 
    { 
     if (stack.Count > 0) 
     { 
      return stack.Pop(); 
     } 
     else if (queue.Count > 0) 
     { 
      return queue.Dequeue(); 
     } 
     return null; 
    } 
} 
+0

questa domanda non è specifica di .NET –

+0

@Neil persone potrebbero puntare a domande precedenti, che hanno soluzioni Java e C++, per un problema come questo. –

+0

e quelle sarebbero soluzioni valide - TDD non è univoco per .NET –

risposta

3

Quando si scrive codice multi-thread, è necessario utilizzare il cervello anche più del solito. È necessario ragionare logicamente su ogni singola riga di codice, indipendentemente dal fatto che sia thread-safe o meno. È come provare la correttezza di una formula matematica - non puoi provare cose come "N + 1> N per tutto N" semplicemente dando esempi di valori di N con cui la formula è vera. Allo stesso modo, provare che una classe è sicura per i thread non è possibile scrivendo casi di test che tentano di esporre problemi con esso. Con un test è possibile solo dimostrare che c'è un difetto, ma non che non ci siano difetti.

La cosa migliore che è possibile fare è ridurre al minimo la necessità di codice multi-thread. Preferibilmente l'applicazione non dovrebbe avere codice multi-thread (ad esempio facendo affidamento su librerie thread-safe e schemi di progettazione adatti), o dovrebbe essere limitato a un'area molto piccola. La tua classe sembra abbastanza semplice, in modo che tu possa renderla sicura in modo sicuro con un po 'di riflessione.

Supponendo che i Stack e Queue implementazioni sono thread-safe (non so le librerie di .NET), è solo bisogno di fare Next() thread-safe. Count è già thread-safe così com'è, poiché nessun client può utilizzare il valore restituito da esso in modo sicuro senza l'utilizzo del blocco basato su client - state dependencies between methods altrimenti infrange il codice.

Next() non è thread-safe, perché ha dipendenze tra i metodi. Se i thread T1 e T2 chiamano allo stesso tempo stack.Count e restituisce 1, uno di questi otterrà il valore con stack.Pop(), ma l'altro chiamerà stack.Pop() quando lo stack è vuoto (che quindi viene visualizzato per generare InvalidOperationException). Avrai bisogno di uno stack e di una coda con le versioni non bloccanti di Pop() e Dequeue() (quelle che restituiscono null quando vuoto).Allora il codice sarebbe thread-safe quando scritti in questo modo:

private WebRequestInfo Next() 
{ 
    WebRequestInfo next = stack.PopOrNull() 
    if (next == null) 
    { 
     next = queue.DequeueOrNull(); 
    } 
    return next; 
} 
+0

Esiste una versione non bloccabile disponibile pubblicamente di .NET Stack ? Sto scrivendo il mio ... Va bene, "non sai le librerie di .NET" scusa –

+0

public class NonBlockingStack : Pila { pubblico T PeekOrDefault() { provare { ritorno this.Peek (); } catch (InvalidOperationException) { return default (T); }} pubblico T PopOrDefault() { provare { ritorno this.Pop(); } catch (InvalidOperationException) { return default (T); } } } –

+0

La mia implementazione sopra utilizza blocchi try-catch per implementare la non-bloccabilità. Immagino che questo non sia il modo migliore. –

1

Giusto per essere chiari, non tutte le classi hanno bisogno di serrature essere thread-safe.

Se i test finiscono per essere eccessivamente complessi, potrebbe essere un sintomo di classi o metodi troppo complicati, due strettamente accoppiati o che assumono troppe responsabilità. Cerca di seguire il principio della singola responsabilità.

Ti dispiacerebbe postare informazioni più concrete sulla tua classe?

+0

ho pubblicato il codice nella domanda corpo –

6

Bene, in genere è possibile utilizzare elementi come ManualResetEvent per ottenere alcuni thread in uno stato di problema previsto prima di rilasciare il gate ... ma questo copre solo un piccolo sottoinsieme di problemi di threading.

Per il problema più grande di bug di collegamento, c'è CHESS (in corso) - forse un'opzione in futuro.

+0

Grazie per l'aiuto. Lo esaminerò. –

+0

Vorrei che Chess fosse più vicino ad essere integrato nell'IDE ... Avevo quasi sperato che VS2010 potesse portare una prima versione. :) – jerryjvl

4

Non si dovrebbe realmente verificare la sicurezza del filo in un test dell'unità. Probabilmente dovresti avere un set separato di stress test per la sicurezza del thread.


Okay, ora che hai postato il codice:

public int Count 
    { 
     get 
     { 
      return this.queue.Count + this.stack.Count; 
     } 
    } 

Questo è un grande esempio di dove si sta andando ad avere problemi a scrivere una prova di unità che esporrà i problemi di threading in il tuo codice. Questo codice potenzialmente ha bisogno di sincronizzazione, perché i valori di this.queue.Count e this.stack.Count possono cambiare nel mezzo del calcolo del totale, in modo che possa restituire un valore che non è "corretto".

TUTTAVIA: visto il resto della definizione della classe, nulla dipende in realtà da Count che fornisce un risultato coerente, quindi è davvero importante se è "sbagliato"? Non c'è modo di saperlo senza sapere come le altre classi del tuo programma usano questo. Ciò rende il test per problemi di threading un test di integrazione, piuttosto che un test unitario.

+0

supponiamo che la classe che sto testando sia una coda thread-safe - come posso testare l'unità in modo significativo senza utilizzare più thread? –

+0

Quello che ho sempre fatto è fare in modo che i test Unit coprano le funzionalità di base della classe - in tal caso, aggiungendo e rimuovendo gli articoli dalla coda. "Thread-safety" non è una proprietà funzionale del codice, dipende dalla situazione.Se sei disposto a impegnarti in questo, puoi inserire nel codice un certo numero di semafori relativi ai test, in modo che tu possa forzare alcuni dei casi "interessanti" di condizioni della competizione, ma non sarai mai essere in grado di ottenerli tutti (interruttore di contesto nel mezzo di una dichiarazione, ad esempio). La revisione attenta del design e i test probabilistici funzionano meglio. –

+0

grazie per l'elaborazione di –

2

Il multithreading può causare problemi così complessi che è quasi impossibile scrivere test di unità per essi. Potresti riuscire a scrivere un test unitario che ha una percentuale di fallimento del 100% quando viene eseguito su un codice, ma dopo averlo fatto passare, è abbastanza probabile che ci siano ancora condizioni di gara e problemi simili nel codice.

Il problema con problemi di threading è che vengono visualizzati casualmente. Anche se fai passare un test unitario, non significa necessariamente che il codice funzioni. Quindi in questo caso TDD dà un falso senso di sicurezza e potrebbe anche essere considerato una cosa negativa.

E vale anche la pena ricordare che se una classe è thread-safe puoi usarla da più thread senza problemi - ma se una classe non è thread-safe non implica immediatamente che non puoi usarla da più discussioni senza problemi.Potrebbe ancora essere thread safe in pratica, ma nessuno vuole assumersi la responsabilità di non essere thread safe. E se in pratica è thread-safe, è impossibile scrivere un test unitario che fallisce a causa del multi-threading. (Ovviamente la maggior parte delle classi non thread-thread non sono thread-safe e falliranno allegramente.)

2

TDD è uno strumento - e buono - ma a volte si hanno problemi che non sono ben risolti usando un strumento particolare. Vorrei suggerire che, se lo sviluppo dei test è eccessivamente complesso, dovresti usare TDD per sviluppare la funzionalità prevista, ma forse fare affidamento sull'ispezione del codice per assicurarti che il codice di blocco che aggiungi sia appropriato e consentirà alla tua classe di essere thread -sicuro.

Una possibile soluzione sarebbe quella di sviluppare il codice che andrebbe all'interno del blocco e inserirlo nel proprio metodo. Quindi potresti essere in grado di simulare questo metodo per testare il tuo codice di blocco. Nel tuo falso codice potresti semplicemente stabilire un'attesa per assicurarti che il secondo thread che accede al codice debba aver atteso il lock fino al completamento del primo thread. Non sapendo esattamente cosa fa il tuo codice, non posso essere più specifico di quello.

+0

Ho anche scritto i test unitari. Ma non li ho pubblicati –

1

Soprattutto con i sistemi multi-core, di solito è possibile verificare i problemi di threading, non solo in modo deterministico.

Il modo in cui di solito lo faccio è far girare più thread che fanno esplodere il codice in questione e contare il numero di risultati imprevisti. Questi thread vengono eseguiti per un breve periodo di tempo (in genere 2-3 secondi), quindi utilizzano Interlocked.CompareExchange per aggiungere i risultati e uscire normalmente. Il mio test che li ha fatti girare poi chiama .Entra su ciascuno, poi controlla se il numero di errori è stato 0.

Certo, non è infallibile, ma con una CPU multi-core, di solito fa abbastanza bene il lavoro per dimostrare ai miei colleghi che c'è un problema che deve essere risolto con l'oggetto (supponendo che l'uso previsto richieda l'accesso multi-thread).

0

maggior parte dei test unitari operano in sequenza e non contemporaneamente, in modo che non esporrà problemi di concorrenza.

L'ispezione del codice da parte dei programmatori con esperienza in concorrenza è la soluzione migliore.

Questi malevoli problemi di concorrenza spesso non verranno visualizzati fino a quando non si avrà abbastanza prodotto sul campo per generare alcune tendenze statisticamente rilevanti. Sono incredibilmente difficili da trovare a volte, quindi è spesso meglio evitare di dover scrivere il codice in primo luogo. Utilizzare, se possibile, librerie pre-esistenti prive di thread e schemi di progettazione ben consolidati.

1

Sono d'accordo con gli altri utenti sul fatto che il codice multithread dovrebbe essere evitato, o almeno confinato in piccole parti della vostra applicazione. Tuttavia, volevo ancora un modo per testare quelle piccole porzioni. Sto lavorando su una porta .NET di MultithreadedTC Java library. La mia porta si chiama Ticking Test e il codice sorgente è pubblicato su Google Code.

MultithreadedTC consente di scrivere una classe di test con diversi metodi contrassegnati con un attributo TestThread. Ogni thread può attendere l'arrivo di un determinato conteggio o affermare ciò che ritiene dovrebbe essere il conteggio corrente. Quando tutti i thread correnti sono bloccati, un thread di coordinatore fa avanzare il conteggio delle zecche e sveglia tutti i thread che sono in attesa del successivo conteggio. Se sei interessato, dai un'occhiata allo MultithreadedTC overview per gli esempi. MultithreadedTC è stato scritto da alcune delle stesse persone che hanno scritto FindBugs.

Ho usato la mia porta con successo su un piccolo progetto. La principale caratteristica mancante è che non ho modo di tracciare i thread appena creati durante il test.

0
  • Trova il tuo "invariante", qualcosa che deve rimanere vero indipendentemente dal numero e dall'interlacciamento dei thread del client. Questa è la parte difficile.
  • Quindi scrivere un test che genera un numero di thread, esercita il metodo e afferma che l'invariante è ancora valido. Questo fallirà - perché non esiste alcun codice per renderlo thread-safe. Rosso
  • Aggiungere il codice per renderlo thread-safe ed eseguire la prova di stress. Verde. Refactor se necessario.

Per ulteriori dettagli, fare riferimento al libro GOOS per i relativi capitoli verso la fine relativi al codice multi-thread.