Test jednostkowy bezpieczeństwa wątku?

Napisałem klasę i wiele testów jednostkowych, ale nie zrobiłem tego bezpiecznie. Teraz chcę, aby wątek klasy był bezpieczny, ale aby to udowodnić i użyć TDD, chcę napisać kilka nieudanych testów jednostkowych, zanim zacznę refaktoryzację.

Jakiś dobry sposób na to?

Moją pierwszą myślą jest po prostu stworzyć kilka wątków i sprawić, by wszystkie używały klasy w niebezpieczny sposób. Zrób to wystarczająco dużo razy z wystarczającą ilością wątków i jestem zobowiązany zobaczyć, że to pęknie.

Author: TheSean, 2009-11-11

9 answers

Są dwa produkty, które mogą ci w tym pomóc:

Oba sprawdzają impas w Twoim kodzie (poprzez test jednostkowy) i myślę, że Szachy sprawdzają również warunki wyścigu.

Korzystanie z obu narzędzi jest łatwe - piszesz prosty test jednostkowy i uruchamiasz kod kilka razy i sprawdzasz, czy w Twoim kodzie są możliwe impasy/warunki wyścigu.

Edit: Google udostępniło narzędzie, które sprawdza dla stanu wyścigu w czasie runtime (nie podczas testów), który wywołał thread-race-test .
nie znajdzie wszystkich warunków wyścigu, ponieważ analizuje tylko bieżący bieg, a nie wszystkie możliwe scenariusze, takie jak narzędzie powyżej, ale może pomóc Ci znaleźć Warunki wyścigu, gdy to nastąpi.

Aktualizacja: Strona Typemock nie ma już linku do Racera i nie była aktualizowana w ciągu ostatnich 4 lat. Projekt został zamknięty.

 21
Author: Dror Helper,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-11 07:10:02

Problem polega na tym, że większość zagadnień wielowątkowych, takich jak warunki rasowe, nie jest deterministyczna ze swojej natury. Mogą one zależeć od zachowania sprzętu, którego nie można emulować ani wyzwalać.

Oznacza to, że nawet jeśli wykonasz testy z wieloma wątkami, nie będą one konsekwentnie zawodzić, jeśli masz wadę w kodzie.

 10
Author: Max Galkin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-11 15:25:13

Zauważ, że odpowiedź Drora nie mówi tego wprost, ale przynajmniej Szachy (i prawdopodobnie Racer) działają, uruchamiając zestaw wątków przez wszystkie możliwe przeplotki, aby uzyskać powtarzalne błędy. Nie tylko uruchamiają wątki przez jakiś czas, mając nadzieję, że jeśli wystąpi błąd, stanie się to przez przypadek.

Na przykład szachy będą przebiegać przez wszystkie przeplotki, a następnie dadzą ci ciąg znaczników, który reprezentuje przeplot, na którym znaleziono impas, dzięki czemu możesz przypisać twoje testy z konkretnymi przeplotami, które są interesujące z perspektywy impasu.

Nie znam dokładnego wewnętrznego działania tego narzędzia i jak mapuje te ciągi znaczników z powrotem do kodu, który możesz zmieniać, aby naprawić impas, ale tam go masz... Naprawdę nie mogę się doczekać, aż to narzędzie (i Pex) stanie się częścią VS IDE.

 5
Author: jerryjvl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-19 03:33:03

Widziałem ludzi próbujących przetestować to za pomocą standardowych testów jednostek, jak sam proponujesz. Testy są powolne i do tej pory nie udało się zidentyfikować ani jednego z problemów współbieżności, z którymi boryka się nasza firma.

Po wielu niepowodzeniach i pomimo mojej miłości do unittestów, doszedłem do wniosku, że błędy w współbieżności nie są jedną z mocnych stron unittestów. Zazwyczaj zachęcam do analizy i recenzji na rzecz unittestów dla klas, w których współbieżność jest tematem. Z całościowym przeglądem system w wielu przypadkach możliwe jest udowodnienie / sfałszowanie twierdzeń o bezpieczeństwie wątku.

W każdym razie chciałbym, aby ktoś dał mi coś, co może wskazywać na coś przeciwnego, więc uważnie przyglądam się temu pytaniu.

 3
Author: daramarak,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-11 16:20:25

Kiedy ostatnio musiałem rozwiązać ten sam problem, pomyślałem o tym w ten sposób; przede wszystkim twoja istniejąca klasa ma jeden obowiązek, a mianowicie zapewnienie pewnej funkcjonalności. To nie przedmiot jest odpowiedzialny za bezpieczeństwo wątku. Jeśli ma być bezpieczny dla wątku, należy użyć innego obiektu, aby zapewnić tę funkcjonalność. Ale jeśli jakiś inny obiekt zapewnia bezpieczeństwo wątku, nie może to być opcjonalne, ponieważ nie możesz udowodnić, że Twój kod jest bezpieczny dla wątku. Więc tak sobie radzę it:

// 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
    }
}

Użycie leków generycznych umożliwia użycie fałszywej blokady w testach, aby sprawdzić, czy blokada jest zawsze podejmowana podczas tworzenia transakcji i nie zwalniana, dopóki transakcja nie zostanie usunięta. Teraz możesz również sprawdzić, czy blokada została podjęta podczas wywoływania ważnej metody. Użycie w kodzie produkcyjnym powinno wyglądać mniej więcej tak:

// 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();
}

Upewniając się, że ImportantImplementation nie może być utworzony przez kogoś innego (na przykład utwórz go w provider i możesz teraz udowodnić, że twoja klasa jest bezpieczna, ponieważ nie można do niej uzyskać dostępu bez transakcji, a transakcja zawsze przyjmuje blokadę po utworzeniu i zwalnia ją po usunięciu.

Upewnij się, że transakcja jest usuwana prawidłowo może być trudniejsze, a jeśli nie, możesz zobaczyć dziwne zachowanie w aplikacji. Możesz użyć narzędzi takich jak Microsoft Chess (jak sugerowano w innym anserze), aby szukać takich rzeczy. Lub możesz zlecić swojemu dostawcy wykonanie elewacji i wykonanie zaimplementuje to tak:

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

Mimo, że jest to implementacja, mam nadzieję, że uda ci się opracować testy, aby zweryfikować te klasy w razie potrzeby.

 2
Author: Cellfish,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-17 04:47:44

TestNG lub Junit z modułem testowym springframeworks (lub innym rozszerzeniem) ma podstawowe wsparcie dla testowania współbieżności.

Ten link może Cię zainteresować

Http://www.cs.rice.edu / ~ javaplt / papers / pppj2009. pdf

 1
Author: surajz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-11 15:29:24

Będziesz musiał skonstruować przypadek testowy dla każdego scenariusza współbieżności; może to wymagać zastąpienia wydajnych operacji wolniejszymi odpowiednikami (lub mockami) i uruchomienia wielu testów w pętlach, aby zwiększyć szansę na zakwestionowanie

Bez konkretnych przypadków testowych trudno jest zaproponować konkretne testy

Niektóre potencjalnie przydatne materiały referencyjne:

 1
Author: Steven A. Lowe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:47:09

Choć nie jest to tak eleganckie jak używanie narzędzia takiego jak Racer czy Chess, używałem tego rodzaju rzeczy do testowania bezpieczeństwa wątku:

// 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++;
    }
}

Teoria jest taka, że jeśli możesz spowodować, że stan niebezpieczny wątku konsekwentnie występuje w bardzo krótkim czasie, powinieneś być w stanie doprowadzić do bezpiecznych warunków wątku i zweryfikować je, czekając stosunkowo dużo czasu bez obserwowania korupcji Państwa.

Przyznam, że może to być prymitywny sposób postępowania i może nie pomóc w złożonych scenariuszach.
 1
Author: Ronnie Overby,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-15 05:18:56

Oto moje podejście. Ten test Nie dotyczy impasów, ale spójności. Testuję metodę z zsynchronizowanym blokiem, z kodem, który wygląda mniej więcej tak:

synchronized(this) {
  int size = myList.size();
  // do something that needs "size" to be correct,
  // but which will change the size at the end.
  ...
}

Trudno jest stworzyć scenariusz, który niezawodnie doprowadzi do konfliktu wątków, ale oto, co zrobiłem.

Najpierw mój test jednostkowy stworzył 50 wątków, uruchomił je wszystkie w tym samym czasie i kazał wywołać moją metodę. Używam zatrzasku odliczania, aby uruchomić je wszystkie w tym samym czas:

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.
To może, ale nie musi, wywołać konflikt. Mój testMethod() powinien być zdolny do rzucania WYJĄTKÓW w przypadku wystąpienia konfliktu. Ale nie możemy być pewni, że spowoduje to konflikt. Więc nie wiemy, czy test jest poprawny. Oto sztuczka: Skomentuj zsynchronizowane słowa kluczowe i uruchom test. jeśli spowoduje to konflikt, test się nie powiedzie. jeśli nie powiedzie się bez zsynchronizowanego słowa kluczowego, twój test jest poprawny.

To właśnie zrobiłem i mój test nie zawiódł, więc nie był (jeszcze) prawidłowym testem. Ale byłem w stanie niezawodnie spowodować awarię, umieszczając powyższy kod w pętli i uruchamiając go 100 razy po kolei. Więc dzwoniłem do metody 5000 razy. (Tak, spowoduje to powolny test. Nie martw się. Twoi klienci nie będą się tym przejmować, więc ty też nie powinieneś.)

Kiedy umieściłem ten kod w zewnętrznej pętli, byłem w stanie niezawodnie zobaczyć awarię około 20 iteracji zewnętrznej pętli. Teraz byłem pewny siebie test był poprawny i przywróciłem zsynchronizowane słowa kluczowe, aby uruchomić rzeczywisty test. (Zadziałało.)

Możesz odkryć, że test jest ważny na jednej maszynie, a nie na innej. Jeśli test jest ważny na jednej maszynie, a twoje metody przechodzą test, to prawdopodobnie jest bezpieczny dla wszystkich maszyn. Ale powinieneś sprawdzić ważność na maszynie, która wykonuje nocne testy jednostkowe.

 0
Author: MiguelMunoz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-08 19:42:55