web-dev-qa-db-fra.com

Async (launch :: async) dans C ++ 11 rend-il les pools de threads obsolètes pour éviter la création coûteuse de threads?

Il est vaguement lié à cette question: Std :: thread est-il mis en pool dans C++ 11? . Bien que la question diffère, l'intention est la même:

Question 1: Est-il toujours judicieux d'utiliser votre propre pool de threads (ou une bibliothèque tierce) pour éviter la création coûteuse de threads?

La conclusion de l’autre question était que vous ne pouvez pas compter sur std::thread Pour être mis en commun (cela peut être ou non). Cependant, std::async(launch::async) semble avoir beaucoup plus de chances d'être mis en commun.

Il ne pense pas que cela soit forcé par la norme, mais à mon humble avis, je m'attendrais à ce que toutes les bonnes implémentations de C++ 11 utilisent le pool de threads si la création de thread est lente. Ce n'est que sur les plates-formes où il est peu coûteux de créer un nouveau thread, je suppose qu'elles engendrent toujours un nouveau thread.

Question 2: C'est exactement ce que je pense, mais je n'ai aucun fait pour le prouver. Je peux très bien me tromper. Est-ce une supposition éclairée?

Enfin, voici un exemple de code qui montre d’abord comment la création de threads peut être exprimée par async(launch::async):

Exemple 1:

 thread t([]{ f(); });
 // ...
 t.join();

devient

 auto future = async(launch::async, []{ f(); });
 // ...
 future.wait();

Exemple 2: Tirez et oubliez le fil

 thread([]{ f(); }).detach();

devient

 // a bit clumsy...
 auto dummy = async(launch::async, []{ f(); });

 // ... but I hope soon it can be simplified to
 async(launch::async, []{ f(); });

Question 3: Préféreriez-vous les versions async aux versions thread?


Le reste ne fait plus partie de la question, mais uniquement à des fins de clarification:

Pourquoi la valeur de retour doit-elle être affectée à une variable muette?

Malheureusement, la norme C++ 11 actuelle oblige à capturer la valeur de retour de std::async, Sinon le destructeur est exécuté et reste bloqué jusqu'à la fin de l'action. C'est considéré par certains comme une erreur dans la norme (par exemple, Herb Sutter).

Cet exemple de cppreference.com illustre bien:

{
  std::async(std::launch::async, []{ f(); });
  std::async(std::launch::async, []{ g(); });  // does not run until f() completes
}

Une autre clarification:

Je sais que les pools de threads peuvent avoir d'autres utilisations légitimes, mais je ne m'intéresse ici qu'à éviter les coûts de création de threads coûteux .

Je pense qu'il existe encore des situations où les pools de threads sont très utiles, en particulier si vous avez besoin de plus de contrôle sur les ressources. Par exemple, un serveur peut décider de ne traiter qu'un nombre fixe de requêtes simultanément afin de garantir des temps de réponse rapides et d'augmenter la prévisibilité de l'utilisation de la mémoire. Les pools de threads devraient être bons, ici.

Les variables locales aux threads peuvent également être un argument pour vos propres pools de threads, mais je ne suis pas certain que cela soit pertinent en pratique:

  • La création d'un nouveau thread avec std::thread Commence sans les variables locales du thread initialisées. Peut-être que ce n'est pas ce que vous voulez.
  • Dans les threads générés par async, il est quelque peu flou pour moi car le thread aurait pu être réutilisé. À ma connaissance, la réinitialisation des variables locales aux unités d'exécution n'est pas garantie, mais je peux me tromper.
  • En revanche, l’utilisation de vos propres pools de threads (de taille fixe) vous donne un contrôle total si vous en avez vraiment besoin.
107
Philipp Claßen

Question 1 :

J'ai changé cela de l'original parce que l'original était faux. J'avais l'impression que la création de threads sous Linux était très économique et après des tests, j'ai déterminé que le temps système nécessaire à l'appel d'une fonction à un nouveau thread est énorme. Le temps système nécessaire à la création d'un thread pour gérer un appel de fonction est environ 10 000 fois plus lent qu'un appel de fonction en clair. Ainsi, si vous émettez beaucoup de petits appels de fonction, un pool de threads peut être une bonne idée.

Il est évident que la bibliothèque C++ standard livrée avec g ++ ne possède pas de pools de threads. Mais je peux certainement voir un cas pour eux. Même avec la surcharge de devoir passer l'appel à travers une sorte de file d'attente inter-thread, cela coûterait probablement moins cher que de démarrer un nouveau thread. Et la norme permet cela.

IMHO, le noyau Linux devrait travailler à rendre la création de fil moins chère qu’elle ne l’est actuellement. Mais la bibliothèque standard C++ devrait également envisager d’utiliser pool pour implémenter launch::async | launch::deferred.

Et l'OP est correct, utiliser ::std::thread Pour lancer un thread force bien sûr la création d'un nouveau thread au lieu de l'utiliser depuis un pool. Donc, ::std::async(::std::launch::async, ...) est préférable.

Question 2 :

Oui, fondamentalement, ceci "implicitement" lance un fil. Mais en réalité, ce qui se passe reste assez évident. Donc, je ne pense pas vraiment que la Parole soit implicitement une Parole particulièrement bonne.

Je ne suis pas non plus convaincu que vous obliger à attendre un retour avant la destruction est nécessairement une erreur. Je ne sais pas si vous devriez utiliser l'appel async pour créer des threads 'daemon' qui ne devraient pas être renvoyés. Et s’ils sont censés revenir, ce n’est pas normal d’ignorer les exceptions.

Question 3 :

Personnellement, j'aime que les lancements de threads soient explicites. J'accorde beaucoup de valeur aux îles où vous pouvez garantir un accès en série. Sinon, vous vous retrouvez avec l'état mutable, vous devez toujours envelopper un mutex quelque part et vous rappeler de l'utiliser.

J'ai aimé le modèle de file d'attente de travail beaucoup mieux que le modèle "futur", car il y a des "îlots de série" qui traînent afin que vous puissiez gérer plus efficacement l'état mutable.

Mais vraiment, cela dépend de ce que vous faites.

Test de performance

Ainsi, j'ai testé les performances de différentes méthodes d'appel et ai trouvé ces numéros sur un système à 8 cœurs (AMD Ryzen 7 2700X) exécutant Fedora 29 compilé avec clang version 7.0.1 et libc ++ (pas libstdc ++):

   Do nothing calls per second:   35365257                                      
        Empty calls per second:   35210682                                      
   New thread calls per second:      62356                                      
 Async launch calls per second:      68869                                      
Worker thread calls per second:     970415                                      

Et natif, sur mon MacBook Pro 15 "(processeur Intel Core (TM) i7-7820HQ à 2.90 GHz) avec Apple LLVM version 10.0.0 (clang-1000.10.44.4) sous OSX 10.13.6, j’obtiens ceci:

   Do nothing calls per second:   22078079
        Empty calls per second:   21847547
   New thread calls per second:      43326
 Async launch calls per second:      58684
Worker thread calls per second:    2053775

Pour le thread de travail, j'ai démarré un thread, puis utilisé une file d'attente sans verrou pour envoyer des demandes à un autre thread, puis j'attendais qu'une réponse "c'est fait" soit renvoyée.

Le "ne rien faire" consiste simplement à tester les frais généraux du faisceau de test.

Il est clair que le lancement d’un fil de discussion est énorme. Et même le thread de travail avec la file d’attente inter-thread ralentit les choses environ 20 fois sur Fedora 25 dans une machine virtuelle et environ 8 fois sous OS X natif.

J'ai créé un projet Bitbucket contenant le code que j'ai utilisé pour le test de performance. Vous pouvez le trouver ici: https://bitbucket.org/omnifarious/launch_thread_performance

42
Omnifarious