await - c# unit test async




Prueba de unidad para seguridad de hilo? (6)

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.


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.


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.





thread-safety