AntonioGanci

Il blog di Antonio Ganci
posts - 201, comments - 420, trackbacks - 31

Come scrivere uno unit test per controllare l'accesso esclusivo ad una risorsa condivisa da diversi thread

Nel mio precedente post ho affrontato il problema di come sviluppare usando il TDD un'applicazione multithreading.

In questo post, cercherò di approfondire su come testare la sincronizzazione per l'accesso ad una risorsa condivisa.

L'obiettivo che voglio raggiungere è scrivere un test che fallisca se non applico un meccanismo di sincronizzazione (in questo caso la lock) per l'accesso alla risorsa e abbia successo quando viene introdotta la sincronizzazione. Il fatto che il test abbia successo deve essere sistematico e non deve esserci nessun elemento di casualità come l'ordine di schedulazione la velocità del processore.

Vediamo di descrivere lo scenario. La risorsa condivisa è il Broker la cui responsabilità è quella di immettere sul mercato azionario un ordine di acquisto o vendita di azioni. La classe Broker implementa la seguente interfaccia:

    interface IBroker
    {
        void PlaceOrder(Order order);
    }

La classe Order che non riporterò contiene i dati necessari per immettere l'ordine come il prezzo, la quantità di azioni, ecc.

L'ultimo elemento di questo scenario è la classe OrderThread, la sua responsabilità è quella di eseguire il metodo PlaceOrder in un thread creato da lei.

    class OrderThread
    {
        private readonly IBroker _broker;
        private readonly Order _order;
 
        public OrderThread(IBroker broker, Order order)
        {
            _broker = broker;
            _order = order;
        }
 
        public void Start()
        {
            Thread thread = new Thread(PlaceOrder);
            thread.Start();
        }
 
        private void PlaceOrder()
        {
            _broker.PlaceOrder(_order);
        }
    }

Come si può notare alla chiamata PlaceOrder non viene usato nessun meccanismo di sincronizzazione. Quindi due istanze di OrderThread che utilizzino lo stesso broker potrebbero chiamare contemporaneamente il metodo PlaceOrder dando luogo ad una race condition. Vogliamo scrivere un test che abbia successo se la race condition non possa verificarsi e fallisca altrimenti.

Per ottenere il risultato scriviamo un'implementazione dell'interfaccia IBroker pilotabile dal nostro test che ci permetta di simulare lo scenario in cui due OrderThread cerchino di chiamare contemporaneamente il metodo PlaceOrder.

    class SleepingBroker : IBroker
    {
        readonly ManualResetEvent _resetEvent = new ManualResetEvent(false);
 
        public int CallingPlaceOrderCount { get; set;}
 
        public void PlaceOrder(Order order)
        {
            CallingPlaceOrderCount++;
            _resetEvent.WaitOne();
            CallingPlaceOrderCount--;
        }
 
        public void WakeUp()
        {
            _resetEvent.Set();
        }
    }

Il metodo SleepingBroker.PlaceOrder ci permette di far durare la chiamata a PlaceOrder il tempo necessario per creare una race condition. Se il valore della property CallingPlaceOrderCount è maggiore di uno significa che più di un trade sta usando il Broker e quindi il test deve fallire.

Proviamo a questo punto a scrivere il test:

        [Test]
        public void Access_To_The_Broker_Should_Be_Synchronized()
        {
            Order order1 = new Order();
            Order order2 = new Order();
            SleepingBroker broker = new SleepingBroker();
            OrderThread thread1 = new OrderThread(broker, order1);
            OrderThread thread2 = new OrderThread(broker, order2);
 
            thread1.Start();
            thread2.Start();
 
            Assert.AreEqual(1, broker.CallingPlaceOrderCount);
 
            broker.WakeUp();
        }

In questo test abbiamo creato due OrderThread che utilizzano lo stesso Broker (in questo caso SleepingBroker). I due thread vengono fatti partire e controlliamo che effettivamente solo uno sia all'interno del metodo PlaceOrder.

Il test apparentemente corretto in realtà contiene un errore concettuale. L'errore è che potrebbe fallire o avere successo in maniera del tutto casuale perchè dipende dalla velocità di creazione ed esecuzione dei thread al momento in cui si fa la Assert.

Il valore di CallingPlaceOrderCount potrebbe quindi essere 0, 1 oppure due. Zero nel caso in cui entrambi i thread siano stati creati ma al momento della Assert la chiamata a PlaceOrder non sia ancora avvenuta. Uno nel caso che uno dei due sia entrato nella PlaceOrder. Due nel caso in cui entrambi siano nella PlaceOrder.

Per come abbiamo scritto il test questo non è determinabile. Quindi modifichiamolo sincronizzandoci nel modo corretto. Per far questo ci viene in aiuto la property ThreadState, nel caso in cui il valore è impostato su WaitSleepJoin come descritto nella MSDN:

The thread is blocked. This could be the result of calling Thread..::.Sleep or Thread..::.Join, of requesting a lock — for example, by calling Monitor..::.Enter or Monitor..::.Wait — or of waiting on a thread synchronization object such as ManualResetEvent.

Quindi se entrambi i thread sono in WaitSleepJoin siamo sicuri di poter eseguire la Assert su CallingPlaceOrderCount nel momento corretto.

Per controllare lo stato del thread Aggiungiamo la Property IsWaiting alla classe OrderThread.

    class OrderThread
    {
        private readonly IBroker _broker;
        private readonly Order _order;
        private Thread _thread;
 
        public OrderThread(IBroker broker, Order order)
        {
            _broker = broker;
            _order = order;
        }
 
        public bool IsWaiting
        {
            get
            {
                if (_thread == null)
                {
                    return false;
                }
                return _thread.ThreadState == ThreadState.WaitSleepJoin;
            }
        }
 
        public void Start()
        {
            _thread = new Thread(PlaceOrder);
            _thread.Start();
        }
 
        private void PlaceOrder()
        {
            _broker.PlaceOrder(_order);
        }
    }

Quindi ora aggiungiamo il codice nel test che attenda che per entrambi i Thread IsWaiting sia a true.

        [Test]
        public void Access_To_The_Broker_Should_Be_Synchronized()
        {
            Order order1 = new Order();
            Order order2 = new Order();
            SleepingBroker broker = new SleepingBroker();
            OrderThread thread1 = new OrderThread(broker, order1);
            OrderThread thread2 = new OrderThread(broker, order2);
 
            thread1.Start();
            thread2.Start();
 
            while(!thread1.IsWaiting || !thread2.IsWaiting)
            {
                Thread.Sleep(0);
            }
 
            Assert.AreEqual(1, broker.CallingPlaceOrderCount);
 
            broker.WakeUp();
        }

In questo modo abbiamo tolto l'elemento di casualità spiegato prima. Se non modifichiamo il codice della classe OrderThread il test fallirà in modo sistematico:

TestCase 'OrderThreadTests.Access_To_The_Broker_Should_Be_Synchronized' failed: Equal assertion failed: [[1]]!=[[2]]

Aggiungiamo a questo punto la lock prima della chiamata di PlaceOrder nella classe OrderThread:

        private void PlaceOrder()
        {
            lock(_broker)
            {
                _broker.PlaceOrder(_order);
            }
        }

Proviamo ora a lanciare nuovamente il test ed ecco finalmente la conferma che i due thread sono sincronizzati:

Found 1 tests
[success] OrderThreadTests.Access_To_The_Broker_Should_Be_Synchronized

Il motivo per cui il test passa è che in questo caso sono entrambi in waiting ma uno è all'interno del metodo PlaceOrder mentre l'altro è fermo sulla lock.

L'esempio che ho fatto si può adattare facilmente anche ad altri scenari, in quanto cambieranno gli attori, ma i concetti sono gli stessi.

Feedback?

Print | posted on domenica 17 agosto 2008 15:46 | Filed Under [ Extreme Programming ]

Feedback

Gravatar

# re: Come scrivere uno unit test per controllare l'accesso esclusivo ad una risorsa condivisa da diversi thread

Non facio diretto riferimento all'esempio che hai postato dove personalmente non mi piace l'approccio basato sui test ma che può funzionare, previo una serie di "se".

Quello che mi preme precisare è che, come ho detto ai recenti community days, unit testing e multithreading proprio non vanno daccordo. Quel giorno ho mostrato un esempio che è sbagliato e, anche eseguito ripetutamente, passa tutti i test.
Poi basta spostarlo su un altro pc o commentare una riga di log ed ecco emergere l'errore.
Non voglio però togliere l'utilità in tanti altri contesti ma certamente unit testing non è una soluzione universale.
18/08/2008 11:58 | Raffaele Rialdi
Gravatar

# re: Come scrivere uno unit test per controllare l'accesso esclusivo ad una risorsa condivisa da diversi thread

Antonio, la correttezza del test non c'entra. Il test è corretto ma a seconda dell'hardware che sta sotto il risultato è quel poco differente che l'esecuzione fornisce risultati differenti.

Il problema è che non hai alcun controllo su quella minima differenza perché dipende da un numero di fattori (tra cui l'hardware e lo scheduler di windows, ma non solo) che rendono indeterministica l'esecuzione.

L'esempio è qui:
http://www.communitydays.it/events/communitydays2008milano.aspx
per usarlo devi installare la beta di PFX.
18/08/2008 18:55 | Raffaele Rialdi
Gravatar

# re: Come scrivere uno unit test per controllare l'accesso esclusivo ad una risorsa condivisa da diversi thread

@Antonio. I fattori che determinano il fallimento sono diversi:
- hardware
- task che girano
- configurazione workstation / server di windows
- etc.
Sul mio portatile il test girava 50 volte correttamente, poi ho commentato un log su disco (ininfluente sull'algoritmo) e ha finalmente cominciato a dare errori.
I fattori che determinano il fallimento (per errata sincronizzazione) NON sono trovabili da unit testing. Questo è un dato di fatto altrimenti non ci sarebbe la corsa a tool di test specifici per il multithreading come Chess o le suite di Intel.

@Luka. Non sto dicendo che unit testing non serva. Dico che ci sono diversi casi, fra cui il multithreading in cui unit testing è fallace e inaffidabile.
Se il test non è più cieco come tu intendi quando vuoi dichiarare uno 'scope' del test, entriamo nella diatriba di "chi testa i test". Scrivere un test come dici tu è molto complesso e richiede un livello di attenzione per cui ti posso contestare l'efficacia dello strumento di unit testing (per quei casi).

Come negli altri recenti thread archiettturali, considero ogni strumento per l'effettiva efficacia e non perché "va usato". Unit testing non è universale e quando vedo che il debugging tradizionale è più efficace di uno unit testing, vado per quella strada. Altrimenti è talebanesimo ;-)
19/08/2008 12:12 | Raffaele Rialdi
Gravatar

# re: Come scrivere uno unit test per controllare l'accesso esclusivo ad una risorsa condivisa da diversi thread

Antonio non ci siamo capiti ancora.
ll mio discorso è generico e non si riferisce specificamente al tuo esempio.
Se per giudicare la bontà di un test dobbiamo vedere a come è fatto il codice da testare, lo unit testing perde di efficacia perché avrai bisogno di testare il test!

Sono daccordo che il tuo codice possa funzionare a prescindere dall'hardware. Ma per asserirlo tu stai guardando il codice e facendo code review. Cioè ti è servita la code review per validare il tuo unit testing. E questo è ovviamente sbagliato dal punto di vista tdd.

Se entri nel merito del codice allora tanto vale investire il tempo per fare altri tipi di validazione invece che in un (a questo punto) inutile unit testing.

Sono intervenuto nel thread non per rompere le uova nel paniere, ma per precisare che ogni strumento ha la sua potenzialità e i suoi limiti. Usare unit testing su codice multithreading non paga perché molto fallace rispetto, ad esempio, ad una normale code review.
Ci sono tool all'orizzonte (lontano) che ci aiuteranno di più ma per adesso questo è il prezzo da pagare.
19/08/2008 17:42 | Raffaele Rialdi
Comments have been closed on this topic.

Powered by:
Powered By Subtext Powered By ASP.NET