Pourquoi la création d'un fil de discussion est-elle coûteuse?

180

Les didacticiels Java indiquent que la création d'un thread coûte cher. Mais pourquoi est-ce cher exactement? Que se passe-t-il exactement lors de la création d'un thread Java qui rend sa création coûteuse? Je considère cette affirmation comme vraie, mais je m'intéresse simplement aux mécanismes de création de threads dans JVM.

Frais généraux du cycle de vie des threads. La création et le démontage de fils ne sont pas gratuits. La surcharge réelle varie selon les plates-formes, mais la création de threads prend du temps, introduisant une latence dans le traitement des demandes et nécessite une certaine activité de traitement de la part de la JVM et du système d'exploitation. Si les demandes sont fréquentes et légères, comme dans la plupart des applications serveur, la création d'un nouveau thread pour chaque demande peut consommer des ressources informatiques importantes.

De Java Concurrency in Practice
Par Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, Doug Lea
Print ISBN-10: 0-321-34960-1

Kachanov
la source
Je ne connais pas le contexte dans lequel les tutoriels que vous avez lus disent ceci: impliquent-ils que la création elle-même est chère, ou que "créer un fil" coûte cher. La différence que j'essaie de montrer est entre l'action pure de créer le thread (appelons-le instancier ou quelque chose), ou le fait que vous ayez un thread (donc en utilisant un thread: ayant évidemment une surcharge). Laquelle est revendiquée // Laquelle souhaitez-vous demander?
Nanne
9
@typoknig - Cher par rapport à NE PAS créer de nouveau fil :)
willcodejavaforfood
duplication possible des frais généraux de création de thread Java
Paul Draper
1
threadpools pour la victoire. pas besoin de toujours créer de nouveaux threads pour les tâches.
Alexander Mills

Réponses:

149

La création de threads Java est coûteuse car il y a pas mal de travail impliqué:

  • Un gros bloc de mémoire doit être alloué et initialisé pour la pile de threads.
  • Des appels système doivent être effectués pour créer / enregistrer le thread natif avec le système d'exploitation hôte.
  • Les descripteurs doivent être créés, initialisés et ajoutés aux structures de données internes à la JVM.

Il est également coûteux en ce sens que le fil attache les ressources tant qu'il est vivant; par exemple, la pile de threads, tous les objets accessibles depuis la pile, les descripteurs de thread JVM, les descripteurs de thread natifs du système d'exploitation.

Les coûts de toutes ces choses sont spécifiques à la plate-forme, mais ils ne sont pas bon marché sur aucune plate-forme Java que je connaisse.


Une recherche sur Google m'a trouvé un ancien benchmark qui rapporte un taux de création de threads d'environ 4000 par seconde sur un Sun Java 1.4.1 sur un Xeon à double processeur vintage 2002 exécutant 2002 vintage Linux. Une plate-forme plus moderne donnera de meilleurs chiffres ... et je ne peux pas commenter la méthodologie ... mais au moins, cela donne une idée du coût probable de la création de threads.

L'analyse comparative de Peter Lawrey indique que la création de threads est beaucoup plus rapide de nos jours en termes absolus, mais on ne sait pas dans quelle mesure cela est dû aux améliorations de Java et / ou du système d'exploitation ... ou à des vitesses de processeur plus élevées. Mais ses chiffres indiquent toujours une amélioration de plus de 150 fois si vous utilisez un pool de threads par rapport à la création / démarrage d'un nouveau thread à chaque fois. (Et il fait remarquer que tout cela est relatif ...)


(Ce qui précède suppose des "threads natifs" plutôt que des "threads verts", mais les JVM modernes utilisent tous des threads natifs pour des raisons de performances. Les threads verts sont peut-être moins chers à créer, mais vous payez pour cela dans d'autres domaines.)


J'ai creusé un peu pour voir comment la pile d'un thread Java est vraiment allouée. Dans le cas d'OpenJDK 6 sous Linux, la pile de threads est allouée par l'appel à pthread_createqui crée le thread natif. (La JVM ne transmet pas de pthread_createpile pré-allouée.)

Puis, à l'intérieur pthread_create la pile est allouée par un appel à mmapcomme suit:

mmap(0, attr.__stacksize, 
     PROT_READ|PROT_WRITE|PROT_EXEC, 
     MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)

Selon man mmap, l' MAP_ANONYMOUSindicateur provoque l'initialisation de la mémoire à zéro.

Ainsi, même s'il n'est peut-être pas essentiel que les nouvelles piles de threads Java soient mises à zéro (selon la spécification JVM), en pratique (au moins avec OpenJDK 6 sous Linux) elles le sont.

Stephen C
la source
2
@Raedwald - c'est la partie d'initialisation qui coûte cher. Quelque part, quelque chose (par exemple le GC ou le système d'exploitation) mettra à zéro les octets avant que le bloc ne soit transformé en pile de threads. Cela prend des cycles de mémoire physique sur du matériel typique.
Stephen C
2
"Quelque part, quelque chose (par exemple le GC ou le système d'exploitation) mettra à zéro les octets". Ce sera? Le système d'exploitation le fera s'il nécessite l'allocation d'une nouvelle page mémoire, pour des raisons de sécurité. Mais ce sera rare. Et le système d'exploitation peut conserver un cache de pages déjà nulles (IIRC, Linux le fait). Pourquoi le GC serait-il dérangé, étant donné que la JVM empêchera tout programme Java de lire son contenu? Notez que la malloc()fonction C standard , que la JVM pourrait très bien utiliser, ne garantit pas que la mémoire allouée soit remise à zéro (probablement pour éviter de tels problèmes de performances).
Raedwald
1
stackoverflow.com/questions/2117072/ ... concède que "un facteur majeur est la mémoire de pile allouée à chaque thread".
Raedwald
2
@Raedwald - voir la réponse mise à jour pour plus d'informations sur la façon dont la pile est réellement allouée.
Stephen C
2
Il est possible (probablement même) que les pages de mémoire allouées par l' mmap()appel soient mappées en copie sur écriture sur une page zéro, de sorte que leur initialisation ne se produit pas en mmap()elle-même, mais lorsque les pages sont écrites pour la première fois , puis une seule page à un temps. Autrement dit, lorsque le thread démarre l'exécution, le coût est assuré par le thread créé plutôt que par le thread créateur.
Raedwald
76

D'autres ont discuté de l'origine des coûts de filetage. Cette réponse explique pourquoi la création d'un thread n'est pas si coûteuse par rapport à de nombreuses opérations, mais relativement coûteuse par rapport aux alternatives d'exécution de tâches, qui sont relativement moins chères.

L'alternative la plus évidente à l'exécution d'une tâche dans un autre thread consiste à exécuter la tâche dans le même thread. Cela est difficile à comprendre pour ceux qui supposent que plus de threads sont toujours meilleurs. La logique est que si la surcharge liée à l'ajout de la tâche à un autre thread est supérieure au temps que vous enregistrez, il peut être plus rapide d'exécuter la tâche dans le thread actuel.

Une autre alternative consiste à utiliser un pool de threads. Un pool de threads peut être plus efficace pour deux raisons. 1) il réutilise les threads déjà créés. 2) vous pouvez régler / contrôler le nombre de threads pour garantir des performances optimales.

Le programme suivant imprime ...

Time for a task to complete in a new Thread 71.3 us
Time for a task to complete in a thread pool 0.39 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 65.4 us
Time for a task to complete in a thread pool 0.37 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 61.4 us
Time for a task to complete in a thread pool 0.38 us
Time for a task to complete in the same thread 0.08 us

Il s'agit d'un test pour une tâche triviale qui expose la surcharge de chaque option de thread. (Cette tâche de test est le type de tâche qui est en fait le mieux exécuté dans le thread actuel.)

final BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
Runnable task = new Runnable() {
    @Override
    public void run() {
        queue.add(1);
    }
};

for (int t = 0; t < 3; t++) {
    {
        long start = System.nanoTime();
        int runs = 20000;
        for (int i = 0; i < runs; i++)
            new Thread(task).start();
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in a new Thread %.1f us%n", time / runs / 1000.0);
    }
    {
        int threads = Runtime.getRuntime().availableProcessors();
        ExecutorService es = Executors.newFixedThreadPool(threads);
        long start = System.nanoTime();
        int runs = 200000;
        for (int i = 0; i < runs; i++)
            es.execute(task);
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in a thread pool %.2f us%n", time / runs / 1000.0);
        es.shutdown();
    }
    {
        long start = System.nanoTime();
        int runs = 200000;
        for (int i = 0; i < runs; i++)
            task.run();
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in the same thread %.2f us%n", time / runs / 1000.0);
    }
}
}

Comme vous pouvez le voir, créer un nouveau thread ne coûte que ~ 70 µs. Cela pourrait être considéré comme trivial dans de nombreux cas d'utilisation, sinon dans la plupart. Relativement parlant, c'est plus cher que les alternatives et pour certaines situations, un pool de threads ou ne pas utiliser de threads du tout est une meilleure solution.

Peter Lawrey
la source
8
C'est un bon morceau de code ici. Concis, au point et affiche clairement son jist.
Nicholas
Dans le dernier bloc, je pense que le résultat est biaisé, car dans les deux premiers blocs, le thread principal se supprime en parallèle au fur et à mesure que les threads de travail se mettent. Cependant dans le dernier bloc, l'action de prendre est entièrement effectuée en série, donc elle dilate la valeur. Vous pouvez probablement utiliser queue.clear () et utiliser un CountDownLatch à la place pour attendre que les threads se terminent.
Victor Grazi
@VictorGrazi Je suppose que vous souhaitez collecter les résultats de manière centralisée. Il effectue la même quantité de travail de mise en file d'attente dans chaque cas. Un verrou de compte à rebours serait légèrement plus rapide.
Peter Lawrey
En fait, pourquoi ne pas simplement lui faire faire quelque chose de manière cohérente et rapide, comme incrémenter un compteur; laissez tomber toute la chose BlockingQueue. Vérifiez le compteur à la fin pour empêcher le compilateur d'optimiser l'opération d'incrémentation
Victor Grazi
@grazi vous pouvez le faire dans ce cas, mais vous ne le feriez pas dans la plupart des cas réalistes, car attendre un compteur pourrait être inefficace. Si vous faisiez cela, la différence entre les exemples serait encore plus grande.
Peter Lawrey
31

En théorie, cela dépend de la JVM. En pratique, chaque thread a une quantité relativement importante de mémoire de pile (256 Ko par défaut, je pense). De plus, les threads sont implémentés comme des threads OS, donc leur création implique un appel OS, c'est-à-dire un changement de contexte.

Sachez que «cher» en informatique est toujours très relatif. La création de threads est très coûteuse par rapport à la création de la plupart des objets, mais pas très chère par rapport à une recherche aléatoire sur le disque dur. Vous n'avez pas à éviter à tout prix de créer des threads, mais en créer des centaines par seconde n'est pas une décision intelligente. Dans la plupart des cas, si votre conception nécessite beaucoup de threads, vous devez utiliser un pool de threads de taille limitée.

Michael Borgwardt
la source
9
Btw kb = kilo-bit, kB = kilo-octet. Gb = giga bit, GB = giga octet.
Peter Lawrey
@PeterLawrey est-ce que nous capitalisons le «k» dans «kb» et «kB», donc il y a une symétrie entre «Gb» et «GB»? Ces choses me dérangent.
Jack
3
@Jack Il y a un K= 1024 et k= 1000.;) en.wikipedia.org/wiki/Kibibyte
Peter Lawrey
9

Il existe deux types de threads:

  1. Les threads appropriés : ce sont des abstractions autour des fonctionnalités de thread du système d'exploitation sous-jacent. La création de threads est donc aussi coûteuse que celle du système - il y a toujours une surcharge.

  2. Threads "verts" : créés et programmés par la JVM, ils sont moins chers, mais aucun paralellisme approprié ne se produit. Ceux-ci se comportent comme des threads, mais sont exécutés dans le thread JVM du système d'exploitation. Ils ne sont pas souvent utilisés, à ma connaissance.

Le facteur le plus important auquel je puisse penser dans la surcharge de création de threads est la taille de pile que vous avez définie pour vos threads. La taille de la pile de threads peut être transmise en tant que paramètre lors de l'exécution de la machine virtuelle.

En dehors de cela, la création de threads dépend principalement du système d'exploitation, et même de l'implémentation de la VM.

Maintenant, permettez-moi de souligner quelque chose: la création de threads coûte cher si vous prévoyez de lancer 2000 threads par seconde, chaque seconde de votre runtime. La JVM n'est pas conçue pour gérer cela . Si vous avez quelques ouvriers stables qui ne seront pas licenciés et tués encore et encore, détendez-vous.

slezica
la source
19
"... un couple d'ouvriers stables qui ne seront pas licenciés et tués ..." Pourquoi ai-je commencé à penser aux conditions de travail? :-)
Stephen C
6

La création Threadsnécessite d'allouer une bonne quantité de mémoire car elle doit créer non pas une, mais deux nouvelles piles (une pour le code java, une pour le code natif). Utilisation d' exécuteurs / pools de threads peut éviter la surcharge, en réutilisant les threads pour plusieurs tâches pour Executor .

Philippe JF
la source
@Raedwald, quel est le jvm qui utilise des piles séparées?
bestsss
1
Philip JP dit 2 piles.
Raedwald
Autant que je sache, toutes les machines virtuelles Java allouent deux piles par thread. Il est utile pour le garbage collection de traiter le code Java (même lorsqu'il est JITed) différemment du free-casting c.
Philip JF
@Philip JF Pouvez-vous préciser? Qu'entendez-vous par 2 piles, une pour le code Java et une pour le code natif? Qu'est ce que ça fait?
Gurinder
"Autant que je sache, toutes les JVM allouent deux piles par thread." - Je n'ai jamais vu aucune preuve qui étayerait cela. Vous ne comprenez peut-être pas la vraie nature de l'opstack dans la spécification JVM. (C'est une façon de modéliser le comportement des bytecodes, pas quelque chose qui doit être utilisé au moment de l'exécution pour les exécuter.)
Stephen C
1

De toute évidence, le nœud de la question est de savoir ce que signifie «cher».

Un thread doit créer une pile et initialiser la pile en fonction de la méthode d'exécution.

Il doit mettre en place des structures d'état de contrôle, c'est-à-dire dans quel état il est exécutable, en attente, etc.

Il y a probablement beaucoup de synchronisation autour de la configuration de ces choses.

MoiBigFatGuy
la source