Test unitaire pour la sécurité des threads?

J’ai écrit une classe et de nombreux tests unitaires, mais je ne l’ai pas rendu sûr. Maintenant, je veux rendre le thread de classe sûr, mais pour le prouver et utiliser TDD, je veux écrire des tests unitaires défaillants avant de commencer le refactoring.

Un bon moyen de faire ça?

Ma première pensée est juste de créer un couple de threads et de les faire tous utiliser la classe d’une manière dangereuse. Faites-le assez de fois avec assez de threads et je suis obligé de le voir casser.

Deux produits peuvent vous aider:

  • Microsoft Chess
  • Coureur Typemock

Les deux vérifient les impasses dans votre code (via le test unitaire) et je pense que Chess vérifie également les conditions de course.

Utiliser les deux outils est facile – vous écrivez un test unitaire simple et vous exécutez votre code plusieurs fois et vérifiez si les conditions de blocage / de course sont possibles dans votre code.

Edit: Google a publié un outil qui vérifie les conditions de course lors de l’exécution (pas pendant les tests) qui s’appelle test de course de thread .
il ne trouvera pas toutes les conditions de course car il parsing seulement l’exécution en cours et pas tous les scénarios possibles comme l’outil ci-dessus, mais cela pourrait vous aider à trouver la condition de course une fois qu’elle se produit.

Mise à jour: le site Typemock n’a plus de lien vers Racer et n’a pas été mis à jour au cours des 4 dernières années. Je suppose que le projet était fermé.

Le problème est que la plupart des problèmes de multithreading, comme les conditions de course, ne sont pas déterministes par nature. Ils peuvent dépendre du comportement du matériel que vous ne pouvez pas émuler ou déclencher.

Cela signifie que même si vous effectuez des tests avec plusieurs threads, ils ne seront pas systématiquement défaillants si vous avez un défaut dans votre code.

Notez que la réponse de Dror ne le dit pas explicitement, mais au moins Chess (et probablement Racer) fonctionne en exécutant un ensemble de threads dans tous leurs liens possibles pour obtenir des erreurs reproductibles. Ils ne font pas que lancer les threads en espérant que s’il ya une erreur, cela se produira par hasard.

Les échecs, par exemple, parcourront tous les liens et vous donneront une chaîne de balises représentant l’imbrication dans laquelle une impasse a été trouvée, afin que vous puissiez atsortingbuer vos tests aux interlivages spécifiques intéressants du sharepoint vue de l’impasse.

Je ne connais pas le fonctionnement interne exact de cet outil, et comment il associe ces chaînes de code à du code que vous pouvez modifier pour corriger une impasse, mais vous l’avez… Je suis vraiment impatient de cet outil ( et Pex) faisant partie de l’IDE VS.

J’ai vu des gens essayer de tester cela avec des désinscriptions standard, comme vous le proposez vous-même. Les tests sont lents et n’ont jusqu’à présent pas permis d’identifier l’un des problèmes de concurrence auxquels notre société est confrontée.

Après de nombreux échecs, et malgré mon amour pour les démunis, j’ai fini par accepter que les erreurs dans la concurrence ne sont pas une force inattendue. J’encourage généralement l’parsing et la révision en faveur des démembrements pour les classes où la concurrence est un sujet. Avec une vue d’ensemble complète du système, il est souvent possible de prouver / falsifier des allégations de sécurité des fils.

Quoi qu’il en soit, j’aimerais que quelqu’un me donne quelque chose qui pourrait indiquer le contraire, alors je regarde cette question de près.

Lorsque j’ai dû récemment faire face au même problème, j’y ai pensé de cette façon; Tout d’abord, votre classe existante a une seule responsabilité, celle de fournir des fonctionnalités. Ce n’est pas la responsabilité des objects d’être thread-safe. S’il doit être thread-safe, un autre object doit être utilisé pour fournir cette fonctionnalité. Mais si un autre object fournit la sécurité du thread, il ne peut pas être facultatif car vous ne pouvez pas prouver que votre code est thread-safe. Voici comment je le gère:

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

L’utilisation de génériques permet d’utiliser un faux locking dans vos tests pour vérifier que le verrou est toujours pris lorsqu’une transaction est créée et non libérée jusqu’à ce que la transaction soit supprimée. Maintenant, vous pouvez également vérifier que le locking est pris lorsque votre méthode importante est appelée. L’utilisation dans le code de production devrait ressembler à ceci:

 // Make sure this is the only way to create ImportantImplementation. // Consider making ImportantImplementation an internal class of the provider. ImportantProvider provider = new ImportantProvider(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(); } 

En vous assurant que ImportantImplementation ne peut pas être créé par quelqu’un d’autre (par exemple en le créant dans le fournisseur et en en faisant une classe privée), vous pouvez maintenant prouver que votre classe est thread-safe. verrouiller lorsqu’il est créé et le libère lorsqu’il est éliminé.

Assurez-vous que la transaction est éliminée correctement peut être plus difficile et sinon vous pourriez voir un comportement étrange dans votre application. Vous pouvez utiliser des outils comme Microsoft Chess (comme suggéré dans une autre réponse) pour rechercher des choses comme ça. Ou vous pouvez demander à votre fournisseur d’implémenter la façade et de la mettre en œuvre comme ceci:

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

Bien que ce soit l’implémentation, j’espère que vous pourrez trouver les tests pour vérifier ces classes si nécessaire.

testNG ou le module de test Junit with springframeworks (ou autre extension) prend en charge les tests de concurrence.

Ce lien pourrait vous intéresser

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

vous devrez créer un scénario de test pour chaque scénario de concurrence concerné; cela peut nécessiter le remplacement d’opérations efficaces avec des équivalents plus lents (ou des simulacres) et l’exécution de plusieurs tests dans des boucles, pour augmenter les chances de contestation.

sans cas de test spécifiques, il est difficile de proposer des tests spécifiques

des documents de référence potentiellement utiles:

  • Le blog d’Oren Ellenbogen
  • Blog de tranche verticale
  • possible question en double SO

Bien que ce ne soit pas aussi élégant que d’utiliser un outil comme Racer ou Chess, j’ai utilisé ce genre de chose pour tester la sécurité des threads:

 // 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 Until(TimeSpan duration) { var until = DateTime.Now.Add(duration); ulong i = 0; while (DateTime.Now < until) { yield return i++; } } IEnumerable WhileTrue() { ulong i = 0; while (true) { yield return i++; } } 

La théorie est que si vous pouvez faire en sorte qu’une condition dangereuse de thread se produise de manière constante dans un laps de temps très court, vous devriez pouvoir créer des conditions sécurisées et les vérifier en attendant un temps relativement long sans observer de corruption.

J’admets que cela peut être une méthode primitive et peut ne pas aider dans des scénarios complexes.

Voici mon approche Ce test ne concerne pas les blocages, il concerne la cohérence. Je teste une méthode avec un bloc synchronisé, avec un code qui ressemble à ceci:

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

Il est difficile de produire un scénario qui produira de manière fiable un conflit de threads, mais voici ce que j’ai fait.

Tout d’abord, mon test unitaire a créé 50 threads, les a tous lancés en même temps et les a tous appelés. J’utilise un verrou CountDown pour les démarrer tous en même temps:

 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. 

Cela peut ou non produire un conflit. Mon testMethod () doit être capable de lancer une exception en cas de conflit. Mais nous ne pouvons pas encore être sûr que cela générera un conflit. Donc, nous ne soaps pas si le test est valide. Voici donc l'astuce: commentez vos mots clés synchronisés et lancez le test. Si cela génère un conflit, le test échouera. S'il échoue sans le mot clé synchronisé, votre test est valide.

C'est ce que j'ai fait et mon test n'a pas échoué, donc ce n'était pas (encore) un test valide. Mais j'ai réussi à générer une panne en plaçant le code ci-dessus dans une boucle et en l'exécutant 100 fois de suite. Donc, j'appelle la méthode 5000 fois. (Oui, cela produira un test lent. Ne vous en faites pas. Vos clients ne seront pas dérangés par cela, alors vous ne devriez pas non plus.)

Une fois que j'ai mis ce code à l'intérieur d'une boucle externe, j'ai pu constater de manière fiable un échec à propos de la 20ème itération de la boucle externe. J'étais maintenant convaincu que le test était valide et j'ai restauré les mots-clés synchronisés pour exécuter le test réel. (Ça a marché.)

Vous pouvez découvrir que le test est valide sur une machine et non sur une autre. Si le test est valide sur une machine et que vos méthodes réussissent le test, il est vraisemblable qu'il soit compatible avec toutes les machines. Mais vous devriez tester la validité sur la machine qui exécute vos tests unitaires nocturnes.