Contexte
Récemment, j'ai posté une classe de minuterie pour examen sur Code Review. J'avais le pressentiment qu'il y avait des bugs de concurrence car j'avais vu un test unitaire échouer une fois, mais je n'avais pas pu reproduire l'échec. C'est pourquoi j'ai posté sur code review.
J'ai reçu des commentaires formidables mettant en évidence diverses conditions de concurrence dans le code. (Je pensais) avoir compris le problème et la solution, mais avant d'apporter des corrections, je voulais exposer les bugs avec un test unitaire. Lorsque j'ai essayé, j'ai réalisé que c'était difficile. Diverses réponses sur Stack Exchange suggéraient que je devrais contrôler l'exécution des threads pour exposer le(s) bug(s) et que tout timing artificiel ne serait pas nécessairement portable sur une autre machine. Cela semblait être une complexité accidentelle au-delà du problème que j'essayais de résoudre.
À la place, j'ai essayé d'utiliser le meilleur outil d'analyse statique (SA) pour Python, PyLint, pour voir s'il repérerait certains des bugs, mais il n'a pas pu le faire. Pourquoi un humain peut-il trouver les bugs à travers une revue de code (essentiellement de l'AS), mais un outil d'AS ne le pourrait pas?
Effrayé à l'idée d'essayer de faire fonctionner Valgrind avec Python (ce qui semblait être du yak-shaving), j'ai décidé de m'attaquer à la correction des bugs sans les reproduire d'abord. Maintenant, je suis dans une impasse.
Voici maintenant le code.
from threading import Timer, Lock
from time import time
class NotRunningError(Exception): pass
class AlreadyRunningError(Exception): pass
class KitchenTimer(object):
'''
Modélise de manière lâche un minuteur de cuisine mécanique avec les différences suivantes:
Vous pouvez démarrer le minuteur avec une durée arbitraire (par exemple 1,2 secondes).
Le minuteur appelle une fonction donnée quand le temps est écoulé.
La requête du temps restant a une précision de 0,1 seconde.
'''
PRECISION_NUM_DECIMAL_PLACES = 1
RUNNING = "RUNNING"
STOPPED = "STOPPED"
TIMEUP = "TIMEUP"
def __init__(self):
self._stateLock = Lock()
with self._stateLock:
self._state = self.STOPPED
self._timeRemaining = 0
def start(self, duration=1, whenTimeup=None):
'''
Démarre le minuteur pour compter à rebours à partir de la durée donnée et appelle whenTimeup quand le temps est écoulé.
'''
with self._stateLock:
if self.isRunning():
raise AlreadyRunningError
else:
self._state = self.RUNNING
self.duration = duration
self._userWhenTimeup = whenTimeup
self._startTime = time()
self._timer = Timer(duration, self._whenTimeup)
self._timer.start()
def stop(self):
'''
Arrête le minuteur, empêchant le rappel de whenTimeup.
'''
with self._stateLock:
if self.isRunning():
self._timer.cancel()
self._state = self.STOPPED
self._timeRemaining = self.duration - self._elapsedTime()
else:
raise NotRunningError()
def isRunning(self):
return self._state == self.RUNNING
def isStopped(self):
return self._state == self.STOPPED
def isTimeup(self):
return self._state == self.TIMEUP
@property
def timeRemaining(self):
if self.isRunning():
self._timeRemaining = self.duration - self._elapsedTime()
return round(self._timeRemaining, self.PRECISION_NUM_DECIMAL_PLACES)
def _whenTimeup(self):
with self._stateLock:
self._state = self.TIMEUP
self._timeRemaining = 0
if callable(self._userWhenTimeup):
self._userWhenTimeup()
def _elapsedTime(self):
return time() - self._startTime
Question
Dans le contexte de cet exemple de code, comment puis-je exposer les conditions de concurrence, les corriger et prouver qu'elles sont corrigées?
Points supplémentaires
Points supplémentaires pour un cadre de test adapté à d'autres implémentations et problèmes, plutôt que spécifiquement à ce code.
Conclusion
Ma conclusion est que la solution technique pour reproduire les conditions de concurrence identifiées est de contrôler la synchronisation de deux threads pour garantir qu'ils s'exécutent dans l'ordre qui exposera un bug. Le point important ici est qu'il s'agit de conditions de concurrence déjà identifiées. La meilleure façon que j'ai trouvée pour identifier les conditions de concurrence est de soumettre votre code à une revue de code et d'encourager des personnes plus expertes à l'analyser.