async - c# test await
Prueba de unidad para seguridad de hilo? (9)
He escrito una clase y muchas pruebas de unidad, pero no lo hice seguro. Ahora, quiero que el hilo de la clase sea seguro, pero para probarlo y usar TDD, quiero escribir algunas pruebas de unidad que fallan antes de comenzar a refactorizar.
¿Alguna buena forma de hacer esto?
Mi primer pensamiento es simplemente crear un par de hilos y hacer que todos usen la clase de una manera insegura. Haga esto suficientes veces con suficientes hilos y estoy seguro de que se romperá.
Aquí está mi enfoque. Esta prueba no se ocupa de los bloqueos, se trata de consistencia. Estoy probando un método con un bloque sincronizado, con un código que se ve así:
synchronized(this) {
int size = myList.size();
// do something that needs "size" to be correct,
// but which will change the size at the end.
...
}
Es difícil producir un escenario que produzca un conflicto de hilos de manera confiable, pero esto es lo que hice.
Primero, mi prueba de unidad creó 50 hilos, los lanzó todos al mismo tiempo, y todos ellos llamaron a mi método. Yo uso un CountDown Latch para iniciarlos todos al mismo tiempo:
CountDownLatch latch = new CountDownLatch(1);
for (int i=0; i<50; ++i) {
Runnable runner = new Runnable() {
latch.await(); // actually, surround this with try/catch InterruptedException
testMethod();
}
new Thread(runner, "Test Thread " +ii).start(); // I always name my threads.
}
// all threads are now waiting on the latch.
latch.countDown(); // release the latch
// all threads are now running the test method at the same time.
Esto puede o no generar un conflicto. My testMethod () debería ser capaz de lanzar una excepción si ocurre un conflicto. Pero aún no podemos estar seguros de que esto genere un conflicto. Entonces no sabemos si la prueba es válida. Así que este es el truco: Comente sus palabras clave sincronizadas y ejecute la prueba. Si esto produce un conflicto, la prueba fallará. Si falla sin la palabra clave sincronizada, su prueba es válida.
Eso fue lo que hice, y mi prueba no falló, por lo que no fue (todavía) una prueba válida. Pero pude producir una falla confiable al colocar el código anterior dentro de un bucle y ejecutarlo 100 veces consecutivas. Así que llamo al método 5000 veces. (Sí, esto producirá una prueba lenta. No se preocupe, sus clientes no se molestarán con esto, por lo que tampoco debería hacerlo).
Una vez que coloqué este código dentro de un bucle externo, pude ver de manera confiable una falla en la vigésima iteración del bucle externo. Ahora estaba seguro de que la prueba era válida y restauré las palabras clave sincronizadas para ejecutar la prueba real. (Funcionó.)
Puede descubrir que la prueba es válida en una máquina y no en otra. Si la prueba es válida en una máquina y sus métodos pasan la prueba, entonces es presumiblemente seguro para subprocesos en todas las máquinas. Pero debe probar la validez de la máquina que ejecuta las pruebas de su unidad nocturna.
Aunque no es tan elegante como usar una herramienta como Racer o Chess, he usado este tipo de cosas para probar la seguridad de las hebras:
// from linqpad
void Main()
{
var duration = TimeSpan.FromSeconds(5);
var td = new ThreadDangerous();
// no problems using single thread (run this for as long as you want)
foreach (var x in Until(duration))
td.DoSomething();
// thread dangerous - it won''t take long at all for this to blow up
try
{
Parallel.ForEach(WhileTrue(), x =>
td.DoSomething());
throw new Exception("A ThreadDangerException should have been thrown");
}
catch(AggregateException aex)
{
// make sure that the exception thrown was related
// to thread danger
foreach (var ex in aex.Flatten().InnerExceptions)
{
if (!(ex is ThreadDangerException))
throw;
}
}
// no problems using multiple threads (run this for as long as you want)
var ts = new ThreadSafe();
Parallel.ForEach(Until(duration), x =>
ts.DoSomething());
}
class ThreadDangerous
{
private Guid test;
private readonly Guid ctrl;
public void DoSomething()
{
test = Guid.NewGuid();
test = ctrl;
if (test != ctrl)
throw new ThreadDangerException();
}
}
class ThreadSafe
{
private Guid test;
private readonly Guid ctrl;
private readonly object _lock = new Object();
public void DoSomething()
{
lock(_lock)
{
test = Guid.NewGuid();
test = ctrl;
if (test != ctrl)
throw new ThreadDangerException();
}
}
}
class ThreadDangerException : Exception
{
public ThreadDangerException() : base("Not thread safe") { }
}
IEnumerable<ulong> Until(TimeSpan duration)
{
var until = DateTime.Now.Add(duration);
ulong i = 0;
while (DateTime.Now < until)
{
yield return i++;
}
}
IEnumerable<ulong> WhileTrue()
{
ulong i = 0;
while (true)
{
yield return i++;
}
}
La teoría es que si puede causar que una condición peligrosa de subproceso ocurra constantemente en un período de tiempo muy corto, debería ser capaz de generar condiciones de seguridad de subprocesos y verificarlos esperando una cantidad relativamente grande de tiempo sin observar la corrupción del estado.
Admito que esta puede ser una forma primitiva de abordarlo y puede que no ayude en escenarios complejos.
Cuando recientemente tuve que abordar el mismo problema, lo pensé de esta manera; En primer lugar, su clase existente tiene una responsabilidad y es proporcionar alguna funcionalidad. No es responsabilidad de los objetos ser seguro para los hilos. Si necesita ser seguro para subprocesos, se debe usar algún otro objeto para proporcionar esta funcionalidad. Pero si algún otro objeto proporciona seguridad de subprocesos, no puede ser opcional porque no puede probar que su código es seguro para subprocesos. Así que así es como lo manejo:
// This interface is optional, but is probably a good idea.
public interface ImportantFacade
{
void ImportantMethodThatMustBeThreadSafe();
}
// This class provides the thread safe-ness (see usage below).
public class ImportantTransaction : IDisposable
{
public ImportantFacade Facade { get; private set; }
private readonly Lock _lock;
public ImportantTransaction(ImportantFacade facade, Lock aLock)
{
Facade = facade;
_lock = aLock;
_lock.Lock();
}
public void Dispose()
{
_lock.Unlock();
}
}
// I create a lock interface to be able to fake locks in my tests.
public interface Lock
{
void Lock();
void Unlock();
}
// This is the implementation I want in my production code for Lock.
public class LockWithMutex : Lock
{
private Mutex _mutex;
public LockWithMutex()
{
_mutex = new Mutex(false);
}
public void Lock()
{
_mutex.WaitOne();
}
public void Unlock()
{
_mutex.ReleaseMutex();
}
}
// This is the transaction provider. This one should replace all your
// instances of ImportantImplementation in your code today.
public class ImportantProvider<T> where T:Lock,new()
{
private ImportantFacade _facade;
private Lock _lock;
public ImportantProvider(ImportantFacade facade)
{
_facade = facade;
_lock = new T();
}
public ImportantTransaction CreateTransaction()
{
return new ImportantTransaction(_facade, _lock);
}
}
// This is your old class.
internal class ImportantImplementation : ImportantFacade
{
public void ImportantMethodThatMustBeThreadSafe()
{
// Do things
}
}
El uso de genéricos hace posible usar un bloqueo falso en sus pruebas para verificar que el bloqueo siempre se toma cuando se crea una transacción y no se libera hasta que se elimina la transacción. Ahora también puede verificar que se realiza el bloqueo cuando se llama a su método importante. El uso en el código de producción debería verse más o menos así:
// Make sure this is the only way to create ImportantImplementation.
// Consider making ImportantImplementation an internal class of the provider.
ImportantProvider<LockWithMutex> provider =
new ImportantProvider<LockWithMutex>(new ImportantImplementation());
// Create a transaction that will be disposed when no longer used.
using (ImportantTransaction transaction = provider.CreateTransaction())
{
// Access your object thread safe.
transaction.Facade.ImportantMethodThatMustBeThreadSafe();
}
Al asegurarse de que la ImportantImplementation no pueda ser creada por otra persona (por ejemplo, crearla en el proveedor y convertirla en una clase privada) ahora puede probar que su clase es segura, ya que no se puede acceder sin una transacción y la transacción siempre toma el bloquear cuando se crea y lo libera cuando se desecha.
Asegúrese de que la transacción se elimine correctamente puede ser más difícil y, si no, puede ver un comportamiento extraño en su aplicación. Puede usar herramientas como Microsoft Chess (como se sugiere en otro mensaje) para buscar cosas como esa. O puede hacer que su proveedor implemente la fachada y la haga implementar de esta manera:
public void ImportantMethodThatMustBeThreadSafe()
{
using (ImportantTransaction transaction = CreateTransaction())
{
transaction.Facade.ImportantMethodThatMustBeThreadSafe();
}
}
Aunque esta es la implementación, espero que pueda averiguar las pruebas para verificar estas clases según sea necesario.
El problema es que la mayoría de los problemas de multi-threading, como las condiciones de carrera, no son deterministas por su naturaleza. Pueden depender del comportamiento del hardware que no es posible emular o activar.
Eso significa que, incluso si realiza pruebas con varios subprocesos, no fallarán consistentemente si tiene un defecto en su código.
Hay dos productos que pueden ayudarlo allí:
Ambos verifican los puntos muertos en su código (a través de la prueba unitaria) y creo que el Chess también verifica las condiciones de la carrera.
Usar ambas herramientas es fácil: usted escribe una prueba simple de la unidad y ejecuta su código varias veces y verifica si el código tiene posibles bloqueos / condiciones de carrera.
Editar: Google lanzó una herramienta que verifica el estado de la carrera en tiempo de ejecución (no durante las pruebas) que llamó thread-race-test .
no encontrará todas las condiciones de carrera porque solo analiza la ejecución actual y no todos los escenarios posibles, como la herramienta anterior, pero podría ayudarlo a encontrar la condición de carrera una vez que ocurra.
Actualización: el sitio de Typemock ya no tenía un enlace a Racer, y no se había actualizado en los últimos 4 años. Supongo que el proyecto estaba cerrado.
He visto a gente tratar de probar esto con pruebas de unidad estándar como usted mismo propone. Las pruebas son lentas y hasta ahora no hemos podido identificar uno de los problemas de concurrencia con los que nuestra compañía tiene problemas.
Después de muchos fracasos, ya pesar de mi amor por los tests unitarios, he llegado a aceptar que los errores en concurrencia no son una de las fortalezas de los unittest. Por lo general, aliento el análisis y la revisión a favor de unittest para las clases donde la concurrencia es un tema. Con una descripción general del sistema, en muchos casos es posible probar / falsificar declaraciones de seguridad de hilos.
De todos modos, me encantaría que alguien me diera algo que pueda señalar lo contrario, así que observo esta pregunta detenidamente.
Tendrás que construir un caso de prueba para cada escenario concurrente de preocupación; esto puede requerir el reemplazo de operaciones eficientes con equivalentes más lentos (o burlas) y la ejecución de múltiples pruebas en bucles, para aumentar la posibilidad de contenciones.
sin casos de prueba específicos, es difícil proponer pruebas específicas
algún material de referencia potencialmente útil:
Tenga en cuenta que la respuesta de Dror no dice esto explícitamente, pero al menos Chess (y probablemente Racer) funcionan al ejecutar un conjunto de subprocesos a través de todos sus intercalados posibles para obtener errores reprecibles. No solo ejecutan los hilos por un tiempo con la esperanza de que si hay un error ocurrirá por coincidencia.
El ajedrez, por ejemplo, ejecutará todos los entrelazados y luego le dará una cadena de etiquetas que representa el entrelazado en el que se encontró un interbloqueo para que pueda atribuir sus pruebas con los entrelazados específicos que son interesantes desde una perspectiva de bloqueo.
No conozco el funcionamiento interno exacto de esta herramienta, y cómo correlaciona estas cadenas de etiquetas con el código que puede estar cambiando para arreglar un punto muerto, pero ya lo tiene ... Realmente estoy esperando esta herramienta ( y Pex) convirtiéndose en parte de VS IDE.
testNG o Junit con módulo de prueba de marcos de resorte (u otra extensión) tiene soporte básico para pruebas de concurrencia.
Este enlace puede interesarte