Qu'est-ce que "Busy Spin" dans un environnement multi-thread?
Comment cela est-il utile et comment peut-il être implémenté en Java dans un environnement multi-thread?
En quoi cela peut-il être utile pour améliorer les performances d'une application?
Certaines des autres réponses passent à côté du véritable problème des files d'attente occupées.
À moins que vous ne parliez d'une application où vous voulez économiser de l'énergie électrique, brûler du temps processeur ne constitue pas en soi une mauvaise chose. C'est seulement mauvais quand un autre thread ou processus est prêt à être exécuté. C'est vraiment mauvais lorsqu'un des threads prêts à être exécutés est le thread que votre boucle d'attente-attente attend.
C'est le vrai problème. Un programme normal en mode utilisateur exécuté sur un système d'exploitation normal n'a aucun contrôle sur les threads exécutés sur quels processeurs, un système d'exploitation normal n'a aucun moyen de faire la différence entre un thread qui est occupé à attendre et un thread qui fonctionne, et même si le système d'exploitation savait que le thread était occupé et en attente, il n'aurait aucun moyen de savoir ce que le thread attendait.
Ainsi, il est tout à fait possible que le serveur occupé attende plusieurs millisecondes (pratiquement une éternité) en attendant un événement, tandis que le seul fil capable de le déclencher se trouve sur la touche (c'est-à-dire dans la file d'attente) en attente à son tour d'utiliser un processeur.
L'attente occupée est souvent utilisée dans les systèmes où le contrôle des threads exécutés sur les processeurs est étroit. Attendre occupé peut être le moyen le plus efficace d’attendre un événement lorsque vous savez que le thread qui le provoquera s’exécutera réellement sur un processeur différent. C'est souvent le cas lorsque vous écrivez du code pour le système d'exploitation lui-même ou lorsque vous écrivez une application intégrée en temps réel s'exécutant sous un système d'exploitation en temps réel.
Kevin Walters a écrit sur le cas où le temps d'attente est très court. Un programme ordinaire lié à la CPU et exécuté sur un système d'exploitation ordinaire peut être autorisé à exécuter des millions d'instructions dans chaque tranche de temps. Ainsi, si le programme utilise un verrou rotatif pour protéger une section critique composée de quelques instructions seulement, il est très peu probable qu'un thread perde sa tranche de temps alors qu'il se trouve dans la section critique. Cela signifie que, si le thread A trouve le verrou verrouillé en rotation, il est fort probable que le thread B, qui détient le verrou, soit en fait est exécuté sur un processeur différent. C'est pourquoi il peut être correct d'utiliser des verrous rotatifs dans un programme ordinaire lorsque vous savez qu'il sera exécuté sur un hôte multiprocesseur.
Occupation en attente ou en rotation est une technique dans laquelle un processus vérifie à plusieurs reprises si une condition est vraie au lieu d'appeler la méthode d'attente ou de veille et de libérer le processeur.
1.Il est principalement utile dans les processeurs multicœurs où la condition sera vraie assez rapidement, à savoir en milliseconde ou en micro seconde.
2.L'avantage de ne pas libérer le processeur est que toutes les données et instructions mises en cache restent inchangées, ce qui peut être perdu si ce thread était suspendu sur un cœur et ramené à un autre
Busy spin est l’une des techniques pourattendre les événements sans libérer le processeur. C'est souvent fait pour éviter de perdre des données dans le processeur mis en cache, ce qui est perdu si le thread est suspendu et repris dans un autre noyau.
Ainsi, si vous travaillez sur un système à faible temps de latence où votre unité de traitement des ordres n'a actuellement pas d'ordre, au lieu de mettre en veille ou d'appeler wait()
, vous pouvez simplement boucler, puis à nouveau vérifier la file d'attente pour les nouveaux messages. Ce n’est bénéfique que si vous devez attendre très peu de temps, par exemple. en micro secondes ou nanosecondes.
LMAX Disrupter framework, une bibliothèque de messagerie inter-threads très performante, possède une classe BusySpinWaitStrategy qui repose sur ce concept et utilise une boucle de rotation occupée pour EventProcessors en attente.
Faire tourner/attendre est généralement une mauvaise idée du point de vue des performances. Dans la plupart des cas, il est préférable de dormir et d'attendre un signal lorsque vous êtes prêt à courir, plutôt que de faire tourner. Prenez le scénario où il y a deux threads, et le thread 1 attend que le thread 2 définisse une variable (par exemple, il attend jusqu'au var == true
. Ensuite, il serait occupé à tourner en faisant simplement
while (var == false)
;
Dans ce cas, vous passerez beaucoup de temps à exécuter le thread 2, car lorsque vous vous réveillez, vous exécutez la boucle sans réfléchir. Donc, dans un scénario où vous attendez que quelque chose comme cela se produise, il est préférable de laisser tout le contrôle à thread 2 en vous endormant et en vous le réveillant une fois terminé.
MAIS, dans de rares cas où le temps que vous devez attendre est très court , il est en fait plus rapide que de tourner le verrou . Cela est dû au temps nécessaire pour exécuter les fonctions de signalisation; la rotation est préférable si le temps utilisé est inférieur au temps nécessaire pour effectuer la signalisation. Ainsi, de cette façon, cela pourrait être bénéfique et pourrait réellement améliorer les performances, mais ce n'est certainement pas le cas le plus fréquent.
Spin Waiting signifie que vous attendez constamment qu'une condition se réalise. L'inverse attend un signal (comme une interruption de thread par notify () et wait ()).
Il existe deux manières d’attendre, d’abord semi-active (sommeil/rendement) et active (attente occupée).
En cas d’attente, un programme inactif utilise activement des codes d’opération spéciaux tels que HLT ou NOP ou d’autres opérations gourmandes en temps. D'autres utilisent juste une boucle while pour vérifier si une condition est vraie.
JavaFramework fournit les méthodes Thread.sleep, Thread.yield et LockSupport.parkXXX () permettant à un thread de transmettre le processeur. Le sommeil attend pendant un laps de temps déterminé, mais alwasy dure plus d’une milliseconde, même si une nano seconde a été spécifiée. Il en va de même pour LockSupport.parkNanos (1). Thread.yield permet une résolution de 100ns pour mon système d'exemple (win7 + i5 mobile).
Le problème avec le rendement est la façon dont cela fonctionne. Si le système est pleinement utilisé, le rendement peut prendre jusqu’à 800 ms dans mon scénario de test (100 threads de travail comptant tous un nombre (a + = a;) indéfiniment). Comme le rendement libère le processeur et ajoute le thread à la fin de tous les threads de son groupe de priorités, le rendement est donc instable à moins que le processeur ne soit pas utilisé dans une certaine mesure.
Une attente occupée bloquera un processeur (cœur) pendant plusieurs millisecondes.
Le framework Java (implémentations de la classe de condition de contrôle) utilise une attente active (occupé) pour des périodes inférieures à 1000 ns (1 microseconde). Sur mon système, une invocation moyenne de System.nanoTime prend 160 ns alors attendre, c’est comme vérifier la condition dépenser 160 ns sur nanoTime et répéter.
Donc, fondamentalement, le framework de concurrence de Java (files d’attente, etc.) a quelque chose comme attendre sous un spin de quelques microsecondes et atteindre la période d’attente dans une granularité de N où N est le nombre de nanosecondes pour la vérification des contraintes de temps et attendre au moins une ms (pour ma part). système).
Une attente active active augmente donc l'utilisation mais contribue à la réactivité globale du système.
Lors de la gravure de temps CPU, vous devez utiliser des instructions spéciales pour réduire la consommation d'énergie du cœur exécutant les opérations consommant beaucoup de temps.
Une "rotation occupée" boucle constamment dans un thread pour voir si l'autre thread a terminé son travail. C'est une "mauvaise idée" car elle consomme des ressources car elle attend juste. Les tours les plus occupés ne peuvent même pas dormir, mais tournent aussi vite que possible en attendant que le travail soit terminé. Il est moins coûteux d’avoir le fil en attente notifié directement par l’achèvement du travail et de le laisser dormir jusque-là.
Notez que j'appelle cela une "mauvaise idée", mais il est parfois utilisé sur du code de bas niveau pour minimiser la latence, mais cela est rarement (voire jamais) nécessaire dans le code Java.
Une rotation occupée n’est rien d’autre que boucler jusqu’à ce que le ou les threads soient terminés. Par exemple. Vous avez dit 10 threads, et vous voulez attendre que tous les threads soient terminés, puis vous voulez continuer,
while(ALL_THREADS_ARE_NOT_COMPLETE);
//Continue with rest of the logic
Par exemple, en Java, vous pouvez gérer plusieurs threads avec ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread('' + i);
executor.execute(worker);
}
executor.shutdown();
//With this loop, you are looping over till threads doesn't finish.
while (!executor.isTerminated());
C'est un tour trop occupé, car il consomme des ressources car le processeur n'est pas idéal, mais continue de tourner en boucle. Nous devrions avoir un mécanisme pour notifier le thread principal (Thread parent) pour indiquer que tous les threads sont terminés et que le reste de la tâche peut continuer.
Avec l'exemple précédent, au lieu d'avoir une rotation occupée, vous pouvez utiliser un mécanisme différent pour améliorer les performances.
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread('' + i);
executor.execute(worker);
}
executor.shutdown();
try {
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
log.fatal("Exception ",e);
}