Il s'agit purement d'une question de philosophie de conception dans le contexte du C++.
Est-ce une philosophie de conception correcte de démarrer un thread à partir d'un constructeur? J'ai une bibliothèque dont la seule responsabilité est de gérer de manière asynchrone certaines requêtes par les API exposées. Pour que cela fonctionne, il doit démarrer un thread en interne.
J'ai MyClass
qui est une classe publique exposée depuis ma bibliothèque.
//MyClass.h
MyClass {
private:
std::thread m_member_thread;
}
Est-il correct de démarrer le thread à partir du constructeur comme ceci?
//MyClass.cpp
MyClass::MyClass() {
// Initialize variables of the class and then start the thread
m_member_thread = std::thread(&MyClass::ThreadFunction, this);
}
Ou, est-il préférable d'exposer une méthode que le code utilisateur de ma bibliothèque appelle explicitement pour démarrer le thread?
//MyClass.cpp
MyClass::MyClass() {
}
MyClass::StartThread() {
m_member_thread = std::thread(&MyClass::ThreadFunction, this);
}
Ou cela n'a-t-il vraiment pas d'importance?
PS:
Oui, le fil est léger et ne fait pas beaucoup d'opérations lourdes. Je veux éviter les réponses qui disent que cela dépend de ce que fait le fil, etc. Il s'agit purement d'une question de philosophie de conception.
L'initialisation complète de l'objet dans le constructeur est généralement préférable car elle réduit le nombre d'états et rend ainsi votre objet plus facile à raisonner. Pas de méthodes de démarrage et d'arrêt, sauf si votre conception a vraiment besoin de cet état! De même, les méthodes init qui doivent être appelées après le constructeur sont une odeur de conception massive - si vous ne pouvez pas initialiser un objet, il ne devrait pas encore exister, qui peut être modélisé avec un pointeur nul. Les opérations avec état peuvent également rendre impossible l'utilisation correcte de const
.
Les conseils "les constructeurs ne devraient pas faire de vrai travail" ne s'appliquent pas très bien au C++. Son objectif est que l'exécution d'un constructeur soit très bon marché, similaire au concept "trivialement constructible" de C++. Suite à cet avis, toutes les ressources doivent être acquises avant la construction et transmises en tant qu'arguments au constructeur, par ex. avec une fonction membre d'usine statique publique qui appelle un constructeur privé. Mais cela viole RAII (acquisition de ressources est initialisation, ne la précède pas) et rend difficile l'écriture de code sans exception. Ce n'est pas une bonne approche en C++.
Par conséquent, il est absolument correct de créer un thread dans un constructeur (et de lever une exception en cas de problème).
La bibliothèque standard comprend quelques exemples de comportements associés. Objets fichier tels que std::ifstream
ouvrir/créer correctement tous les fichiers dans leur constructeur. Malheureusement, leur conception est défectueuse en ce que l'objet résultant peut ne pas être dans un état utilisable en cas d'erreur. Une meilleure conception aurait été de générer des erreurs ou d'utiliser une fonction d'usine qui renvoie une valeur facultative. Deuxièmement, considérez les tampons tels que std::vector
ou std::string
. Ils peuvent allouer de la mémoire dans le constructeur, mais ce n'est pas obligatoire. Au lieu de cela, les allocations peuvent être différées jusqu'à ce que des caractères/objets soient ajoutés au tampon et épuisent sa capacité. Dans votre conception, il peut être préférable de différer l'initialisation du thread jusqu'à ce qu'il soit réellement utilisé, par ex. jusqu'à ce qu'une unité de travail soit planifiée pour le thread.
En tant que philosophie de conception générale, c'est une mauvaise idée. Ceci introduit une condition de concurrence.
Le nouveau thread démarre immédiatement et peut accéder aux ressources non encore initialisées de l'objet. En outre, le thread ne peut pas utiliser de méthodes virtuelles. Pour en savoir plus sur ce problème, veuillez lire cet article: https://rafalcieslak.wordpress.com/2014/05/16/c11-stdthreads-managed-by-a-designated-class/
Bien sûr, on peut affirmer qu'en suivant certaines règles, ce serait correct, mais votre code nécessitera à jamais une "attention particulière".
Cela dépend de votre philosophie de conception pour savoir si les constructeurs doivent faire un vrai travail. S'ils y sont autorisés, c'est bien, peu importe ce que fait le fil. Sinon, ce n'est pas le cas.
Pensez également aux invariants de votre classe. Le thread exécuté en arrière-plan est-il un invariant de votre classe? Les invariants doivent être établis dans le constructeur.
Enfin, considérez ceci: le propre constructeur de std::thread
Démarre un thread. Cela peut être un indice de la philosophie de conception des concepteurs de bibliothèques standard.
Pour moi, la question est, en termes de vos besoins, à quel point la durée de vie de votre fil est liée à la durée de vie de l'objet. Comme point de considération, considérez un pool de threads.
class ThreadPool
{
...
private:
MyClass threads[max_threads];
};
Dans ce cas, cela pourrait devenir un fardeau si la durée de vie de vos objets est liée à la durée de vie de vos threads, car le pool de threads peut vouloir allouer/initialiser N
threads et objets correspondants à l'avance et démarrer et arrêter ces threads indépendamment d'un ctor et dtor. Vous pouvez commencer à lutter contre le langage, surtout si la performance devient un problème, plutôt que de travailler en harmonie avec le langage si vos besoins appellent en fait une séparation de ces deux durées de vie (durée de vie du fil vs. durée de vie de l'objet) tout en essayant de lier obstinément les deux ensemble.
Vous souhaiterez peut-être rendre ces deux indépendants l'un de l'autre si vous souhaitez réutiliser une ressource de thread existante et la redémarrer avec un nouvel état de thread et une nouvelle fonction à appeler sans réellement créer un nouveau thread et instancier un nouvel objet correspondant.
Dans un tel cas ci-dessus, vous souhaiterez peut-être un start_thread
ou start
ou run
sorte de méthode pour démarrer l'exécution du thread indépendamment du moment où le wrapper est construit, ainsi qu'une méthode pour terminer le thread avant la destruction de l'objet (bien que ce sera probablement toujours une bonne idée pour assurer la terminaison du fil dans la destruction de l'objet).
C'est donc la principale préoccupation que je considérerais dans ce cas particulier. Cela simplifierait certainement les choses si vous pouvez lier la durée de vie d'un objet à la durée de vie du thread. Pourtant, cela pourrait ouvrir plus d'options de conception si vous ne le faites pas et séparer les deux. Il y a parfois un mérite à séparer "démarrage d'une opération" de "construction d'un objet" ... ou pas et cela pourrait simplement se traduire par plus de code que ce qui serait idéalement nécessaire si vous n'avez jamais réellement besoin de cette distinction.