2009-11-11 11 views
66

He escrito una prueba de clase y muchas unidades, 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.¿Prueba de unidad para seguridad de hilo?

¿Alguna buena forma de hacerlo?

Mi primer pensamiento es simplemente crear un par de hilos y hacer que todos utilicen la clase de una manera insegura. Haga esto suficientes veces con suficientes hilos y estoy seguro de que se romperá.

+0

He realizado pruebas unitarias de la manera descrita anteriormente, pero siempre sentí que hay un elemento de aleatoriedad en el resultado, por lo que seguiré las respuestas aquí con interés: o) –

+3

Duplicado: http: // stackoverflow.com/questions/1715822/unit-test-for-thread-safe-ness – JeffH

+12

@JeffH Sí, tienes razón. Pffffff, deja de tratar de ser un policía SO. –

Respuesta

21

Hay dos productos que pueden ayudarle a no:

Tanto cheque por los puntos muertos en su código (a través de prueba de la unidad) y creo que los cheques de ajedrez para la carrera condiciones también.

Usar ambas herramientas es fácil: usted escribe una prueba de unidad simple y ejecuta su código varias veces y verifica si su código tiene posibles bloqueos/condiciones de carrera.

Editar: Google ha lanzado una herramienta que comprueba la condición de carrera en tiempo de ejecución (no durante las pruebas) que llama 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 Typemock ya no tenía un enlace a Racer, y no fue actualizado en los últimos 4 años. Supongo que el proyecto estaba cerrado.

+0

Creo que el MS Chess linke está roto. Pruebe este: http://research.microsoft.com/en-us/projects/chess/default.aspx – jpbochi

+1

El Typemock Racer también parece estar roto. Pruebe esto: http://site.typemock.com/typemock-racer – jpbochi

+0

El enlace MS Chess parece funcionar a partir de ahora. Se corrigió el enlace de Typemock Racer. –

10

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.

+0

-1 para "... comportamiento del hardware ... no se puede emular ...". Parece posible que, al menos para los casos más simples, el número de combinaciones de acceso de entrelazado sea finito y pueda ser enumerado (aunque "de alguna manera") y luego se obligue al código a ejercitar cada combinación a través de un programador de hilos instrumentado. Algunas afirmaciones dicen que Chess tiene una cobertura del 100%. Si eso es cierto, entonces el hardware no debería ser un factor. – crokusek

1

que tendrá que construir un caso de prueba para cada escenario de concurrencia de interés; esto puede requerir la sustitución de operaciones eficientes con equivalentes más lentas (o simulacros) y la ejecución de múltiples pruebas en bucles, para aumentar la probabilidad de contenciones

sin casos de prueba específicas, es difícil proponer pruebas específicas

alguna referencia potencialmente útil Material:

3

He visto a personas intentar 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 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.

2

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í es cómo 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 que sea posible el uso de una cerradura falsa en sus pruebas para verificar que la cerradura se toma siempre cuando se crea una transacción y no se libera hasta que se dispone de 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 debe ser algo como esto:

// 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 el ImportantImplementation no puede ser creado por otra persona (por ejemplo la creó en el proveedor y hacer que una clase privada) que Kan ahora demostrar su clase es seguro para subprocesos ya que no se puede acceder sin una transacción y la transacción siempre toma el bloqueo cuando se crea y lo libera cuando se elimina.

Asegúrate de que la transacción se resuelva correctamente puede ser más difícil y, si no, podrías ver un comportamiento extraño en tu 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 de implementar la fachada y que sea implementar de esta manera:

public void ImportantMethodThatMustBeThreadSafe() 
    { 
     using (ImportantTransaction transaction = CreateTransaction()) 
     { 
      transaction.Facade.ImportantMethodThatMustBeThreadSafe(); 
     } 
    } 

A pesar de que esta es la aplicación espero que puedan averiguar las pruebas para verificar estas clases, según sea necesario.

5

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 hilos a través de todos sus entrelazados 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 intercalaciones específicas que son interesantes desde una perspectiva de interbloqueo.

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 solucionar un punto muerto, pero ya lo tiene ... Realmente estoy esperando a esta herramienta (y a Pex) convirtiéndose en parte de VS IDE.

1

Aunque no es tan elegante como el uso de una herramienta como corredor o ajedrez, he utilizado este tipo de cosas para las pruebas de seguridad de los subprocesos:

// 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 se puede causar una condición peligrosa hilo Para que ocurra de manera constante en un período de tiempo muy corto, usted debe ser capaz de generar condiciones seguras para el hilo y verificarlas esperando una cantidad relativamente grande de tiempo sin observar la corrupción del estado.

Admito que esta puede ser una forma primitiva de resolverlo y puede que no ayude en escenarios complejos.

0

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 el código que se ve algo como esto:

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 va a producir de manera fiable un conflicto hilo, pero aquí es lo que hice.

Primero, mi prueba de unidad creó 50 hilos, los lancé todos al mismo tiempo, y los hice llamar a todos por mi método. Yo uso una cuenta atrás Latch para empezar a 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 puede producir 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 es lo que hice, y mi prueba no falló, por lo que no era (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 coloque este código dentro de un bucle externo, fue capaz de ver con fiabilidad 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.