Vorrei rispondere ad alcuni commenti del mio precedente post. Il primo di Claudio Maccari
Nel tuo esempio crei un thread per ogni OrderThread e non credo sia la cosa più efficente da fare. meglio usare ThreadPool.QueueUserWorkItem Io scriverei così all'interno di OrderThread
public void Start()
{
ThreadPool.QueueUserWorkItem(delegate { _broker.PlaceOrder(_order); });
}
ma in questo caso non saprei come scrivere uno unit-test adeguato. tu come faresti ?
Per prima cosa cambiamo l'implementazione della classe OrderThread in modo che utilizzi un ThreadPool:
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) > 0;
}
}
public void Start()
{
ThreadPool.QueueUserWorkItem(delegate { PlaceOrder(); });
}
private void PlaceOrder()
{
_thread = Thread.CurrentThread;
_broker.PlaceOrder(_order);
}
}
Ho dovuto modificare l'implementazione della property IsWaiting perchè i thread creati dal ThreadPool sono Background thread.
Ora lanciamo il test che è rimasto uguale rispetto al precedente post. L'output generato è il seguente:
TestCase 'OrderThreadTests.Access_To_The_Broker_Should_Be_Synchronized' failed: Equal assertion failed: [[1]]!=[[2]]
In pratica entrambi i thread sono all'interno del metodo PlaceOrder del Broker! Abbiamo una race condition. Questo è normale perchè non abbiamo sincronizzato l'accesso al Broker.
Modifichiamo quindi il metodo privato PlaceOrder aggiungendo una lock:
private void PlaceOrder()
{
_thread = Thread.CurrentThread;
lock(_broker)
{
_broker.PlaceOrder(_order);
}
}
Ora rilanciamo il test:
[success] OrderThreadTests.Access_To_The_Broker_Should_Be_Synchronized
Ed ecco che abbiamo cambiato l'implentazione della classe OrderTest senza modificare il test. Ci si potrebbe chiedere perchè non ho utilizzato subito un ThreadPool, il motivo è che nell'applicazione questi thread seguono anche lo stato dell'ordine ed hanno quindi una vita molto lunga (un ordine può essere eseguito dopo qualche ora che è stato immesso) e mi serviva avere un riferimento al thread per sincronizzarli tra di loro.
Alcune considerazioni sullo unit testing per il multithreading:
- Lo unit test NON garantisce l'assenza di problemi di sincronizzazione come in generale non garantisce l'assenza di bug. In questo caso però modificando l'implementazione con quella proposta di Claudio ha fatto subito emergere il problema di essersi dimenticato il lock
- Il TDD in questi casi serve soprattutto a far emergere un'architettura facilmente testabile
- Il Multithreading aggiunge molta complessità al codice questo rende difficile anche la scrittura dei test i quali potrebbero dipendere dal pc su cui girano perchè a chi li ha scritti è sfuggita qualche condizione di concorrenza.
Vorrei infine segnalare il post TotT: Sleeping != Synchronization del Google Testing Blog che tratta lo stesso argomento, il codice è scritto in ruby ma non dovrebbero esserci molte difficoltà a comprenderlo.