web-dev-qa-db-fra.com

Forcer plusieurs threads à utiliser plusieurs processeurs lorsqu'ils sont disponibles

J'écris un programme Java qui utilise beaucoup de CPU à cause de la nature de ce qu'il fait. Cependant, beaucoup peuvent fonctionner en parallèle, et j'ai fait mon programme multi-thread . Lorsque je l'exécute, il ne semble utiliser qu'un seul processeur jusqu'à ce qu'il en ait besoin de plus qu'il n'en utilise un autre - y a-t-il tout ce que je peux faire en Java pour forcer différents threads à s'exécuter sur différents cœurs/CPU?

66
Nosrama

Lorsque je l'exécute, il ne semble utiliser qu'un seul processeur jusqu'à ce qu'il en ait besoin de plus qu'il en utilise un autre - y a-t-il quelque chose que je puisse faire en Java pour forcer différents threads à s'exécuter sur différents cœurs/processeurs ?

J'interprète cette partie de votre question comme signifiant que vous avez déjà résolu le problème de rendre votre application multi-thread capable. Et malgré cela, il ne commence pas immédiatement à utiliser plusieurs cœurs.

La réponse à "existe-t-il un moyen de forcer ..." n'est pas (AFAIK) directement. Votre JVM et/ou le système d'exploitation hôte décident du nombre de threads "natifs" à utiliser et de la façon dont ces threads sont mappés aux processeurs physiques. Vous avez quelques options de réglage. Par exemple, j'ai trouvé cette page qui explique comment régler Java threading sur Solaris. Et cette page parle d'autres choses qui peuvent ralentir une application multi-thread.

29
Stephen C

Il existe deux méthodes de base pour multi-thread en Java. Chaque tâche logique que vous créez avec ces méthodes doit s'exécuter sur un nouveau cœur lorsque cela est nécessaire et disponible.

Première méthode: définissez un objet Runnable ou Thread (qui peut prendre un Runnable dans le constructeur) et lancez-le en cours d'exécution avec la méthode Thread.start (). Il s'exécutera sur tout noyau que le système d'exploitation lui donne - généralement le moins chargé.

Tutoriel: Définition et démarrage des threads

Deuxième méthode: définissez les objets implémentant l'interface Runnable (s'ils ne renvoient pas de valeurs) ou Callable (s'ils le font), qui contiennent votre code de traitement. Transmettez-les en tant que tâches à un ExecutorService à partir du package Java.util.concurrent. La classe Java.util.concurrent.Executors a un tas de méthodes pour créer des types standard et utiles d'ExecutorServices. Lien vers le tutoriel des exécuteurs.

Par expérience personnelle, les pools de threads fixes et mis en cache des exécuteurs sont très bons, bien que vous souhaitiez ajuster le nombre de threads. Runtime.getRuntime (). AvailableProcessors () peut être utilisé au moment de l'exécution pour compter les cœurs disponibles. Vous devrez fermer les pools de threads lorsque votre application est terminée, sinon l'application ne se fermera pas car les threads ThreadPool restent en cours d'exécution.

Obtenir de bonnes performances multicœurs est parfois délicat et plein de pièges:

  • Les E/S de disque ralentissent beaucoup quand elles sont exécutées en parallèle. Un seul thread doit faire la lecture/écriture du disque à la fois.
  • La synchronisation des objets assure la sécurité des opérations multithread, mais ralentit le travail.
  • Si les tâches sont trop triviales (petits bits de travail, exécutez rapidement), la surcharge de leur gestion dans un ExecutorService coûte plus cher que ce que vous gagnez avec plusieurs cœurs.
  • La création de nouveaux objets Thread est lente. Les ExecutorServices essaieront de réutiliser les threads existants si possible.
  • Toutes sortes de choses folles peuvent se produire lorsque plusieurs threads fonctionnent sur quelque chose. Gardez votre système simple et essayez de rendre les tâches logiquement distinctes et sans interaction.

Un autre problème: contrôler le travail est difficile! Une bonne pratique consiste à avoir un thread de gestionnaire qui crée et soumet des tâches, puis deux threads de travail avec des files d'attente de travail (à l'aide d'un ExecutorService).

Je touche simplement les points clés ici - la programmation multithread est considérée comme l'un des sujets de programmation les plus difficiles par de nombreux experts. C'est non intuitif, complexe et les abstractions sont souvent faibles.


Édition - Exemple utilisant ExecutorService:

public class TaskThreader {
    class DoStuff implements Callable {
       Object in;
       public Object call(){
         in = doStep1(in);
         in = doStep2(in);
         in = doStep3(in); 
         return in;
       }
       public DoStuff(Object input){
          in = input;
       }
    }

    public abstract Object doStep1(Object input);    
    public abstract Object doStep2(Object input);    
    public abstract Object doStep3(Object input);    

    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        ArrayList<Callable> tasks = new ArrayList<Callable>();
        for(Object input : inputs){
           tasks.add(new DoStuff(input));
        }
        List<Future> results = exec.invokeAll(tasks);
        exec.shutdown();
        for(Future f : results) {
           write(f.get());
        }
    }
}
56
BobMcGee

Tout d'abord, vous devez vous prouver que votre programme fonctionnerait plus rapide sur plusieurs cœurs. De nombreux systèmes d'exploitation s'efforcent d'exécuter des threads de programme sur le même noyau dans la mesure du possible.

L'exécution sur le même noyau présente de nombreux avantages. Le cache du processeur est chaud, ce qui signifie que les données de ce programme sont chargées dans le processeur. Les objets de verrouillage/moniteur/synchronisation sont dans le cache du processeur, ce qui signifie que les autres processeurs n'ont pas besoin d'effectuer des opérations de synchronisation du cache sur le bus (coûteux!).

Une chose qui peut très facilement faire fonctionner votre programme sur le même CPU tout le temps est la surutilisation des verrous et de la mémoire partagée. Vos fils ne devraient pas se parler. Moins vos threads utilisent les mêmes objets dans la même mémoire, plus ils s'exécutent souvent sur des CPU différents. Plus ils utilisent souvent la même mémoire, plus ils doivent bloquer l'attente de l'autre thread.

Chaque fois que le système d'exploitation voit un bloc de thread pour un autre thread, il exécutera ce thread sur le même processeur chaque fois qu'il le pourra. Il réduit la quantité de mémoire qui se déplace sur le bus inter-CPU. C'est ce qui, je suppose, est à l'origine de ce que vous voyez dans votre programme.

18
Zan Lynx

Tout d'abord, je suggère de lire "Concurrence in Practice" par Brian Goetz .

alt text

C'est de loin le meilleur livre décrivant la programmation concurrente Java.

La concurrence est "facile à apprendre, difficile à maîtriser". Je suggère de lire beaucoup sur le sujet avant de l'essayer. Il est très facile d'obtenir un programme multi-thread pour fonctionner correctement 99,9% du temps et échouer 0,1%. Cependant, voici quelques conseils pour vous aider à démarrer:

Il existe deux façons courantes de faire en sorte qu'un programme utilise plusieurs cœurs:

  1. Faites fonctionner le programme en utilisant plusieurs processus. Un exemple est Apache compilé avec le MPM Pre-Fork, qui attribue des requêtes aux processus enfants. Dans un programme multi-processus, la mémoire n'est pas partagée par défaut. Cependant, vous pouvez mapper des sections de mémoire partagée sur plusieurs processus. Apache fait cela avec son "tableau de bord".
  2. Faites le programme multi-thread. Dans un programme multithread, toute la mémoire de tas est partagée par défaut. Chaque thread a toujours sa propre pile, mais peut accéder à n'importe quelle partie du tas. En règle générale, la plupart des programmes Java sont multi-threads et non multi-processus.

Au niveau le plus bas, on peut créer et détruire des threads . Java facilite la création de threads de manière multiplateforme portable.

Comme la création et la destruction des threads ont tendance à coûter cher en permanence, Java inclut désormais Executors pour créer des pools de threads réutilisables. Des tâches peuvent être attribuées aux exécuteurs) et le résultat peut être récupéré via un objet Future.

Généralement, on a une tâche qui peut être divisée en tâches plus petites, mais les résultats finaux doivent être rapprochés. Par exemple, avec un tri par fusion, on peut diviser la liste en parties de plus en plus petites, jusqu'à ce que chaque noyau effectue le tri. Cependant, comme chaque sous-liste est triée, elle doit être fusionnée afin d'obtenir la liste triée finale. Puisqu'il s'agit d'un problème de "diviser pour mieux régner" qui est assez courant, il existe un framework JSR qui peut gérer la distribution et la jointure sous-jacentes. Ce framework sera probablement inclus dans Java 7.

8
brianegge

Il n'y a aucun moyen de définir l'affinité CPU en Java. http://bugs.Sun.com/bugdatabase/view_bug.do?bug_id=4234402

Si vous devez le faire, utilisez JNI pour créer des threads natifs et définir leur affinité.

4
Iouri Goussev

Vous devez écrire votre programme pour faire son travail sous la forme d'un lot de Callable remis à un ExecutorService et exécuté avec invokeAll (...).

Vous pouvez ensuite choisir une implémentation appropriée lors de l'exécution dans la classe Executors. Une suggestion serait d'appeler Executors.newFixedThreadPool () avec un nombre correspondant approximativement au nombre de cœurs de processeur pour rester occupé.

Vous pouvez utiliser ci-dessous l'API de Executors with Java 8 version

public static ExecutorService newWorkStealingPool()

Crée un pool de threads de vol de travail en utilisant tous les processeurs disponibles comme niveau de parallélisme cible.

En raison du mécanisme de vol de travail, les threads inactifs volent les tâches de la file d'attente des tâches des threads occupés et le débit global augmentera.

De grepcode , l'implémentation de newWorkStealingPool est la suivante

/**
     * Creates a work-stealing thread pool using all
     * {@link Runtime#availableProcessors available processors}
     * as its target parallelism level.
     * @return the newly created thread pool
     * @see #newWorkStealingPool(int)
     * @since 1.8
     */
    public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }
1
Ravindra babu

Le réglage des performances de la JVM a déjà été mentionné dans Pourquoi ce Java n'utilise-t-il pas tous les cœurs de processeur? . Notez que cela ne s'applique qu'à la JVM, donc votre application doit déjà utiliser des threads (et plus ou moins "correctement" à cela):

http://ch.Sun.com/sunnews/events/2009/apr/adworkshop/pdf/5-1-Java-Performance.pdf

1
ShiDoiSi

La chose la plus simple à faire est de diviser votre programme en plusieurs processus. Le système d'exploitation les répartira entre les cœurs.

Un peu plus difficile est de diviser votre programme en plusieurs threads et de faire confiance à la JVM pour les allouer correctement. C'est - généralement - ce que les gens font pour utiliser le matériel disponible.


Modifier

Comment un programme multi-traitement peut-il être "plus facile"? Voici une étape dans un pipeline.

public class SomeStep {
    public static void main( String args[] ) {
        BufferedReader stdin= new BufferedReader( System.in );
        BufferedWriter stdout= new BufferedWriter( System.out );
        String line= stdin.readLine();
        while( line != null ) {
             // process line, writing to stdout
             line = stdin.readLine();
        }
    }
}

Chaque étape du pipeline est structurée de manière similaire. 9 lignes de frais généraux pour tout traitement inclus.

Ce n'est peut-être pas le plus efficace. Mais c'est très simple.


La structure globale de vos processus simultanés n'est pas un problème JVM. C'est un problème de système d'exploitation, alors utilisez le Shell.

Java -cp pipline.jar FirstStep | Java -cp pipline.jar SomeStep | Java -cp pipline.jar LastStep

La seule chose qui reste à faire est de définir une sérialisation pour vos objets de données dans le pipeline. La sérialisation standard fonctionne bien. Lisez http://Java.Sun.com/developer/technicalArticles/Programming/serialization/ pour des conseils sur la façon de sérialiser. Vous pouvez remplacer BufferedReader et BufferedWriter par ObjectInputStream et ObjectOutputStream pour accomplir cela.

1
S.Lott

Je pense que ce problème est lié à Java Parallel Proccesing Framework (JPPF). À l'aide de cela, vous pouvez exécuter différents travaux sur différents processeurs.

1
Nandika