2011-07-15 3 views
4

Supposons que vous ayez un objet accessible par de nombreux threads. Une section critique est utilisée pour protéger les zones sensibles. Mais qu'en est-il du destructeur? Même si j'entre une section critique dès que j'entre dans le destructeur, une fois que le destructeur a été appelé, l'objet est-il déjà invalidé?C++: Concurrence et destructeurs

Mon train de pensée: Dites que j'entre dans le destructeur, et je dois attendre sur la section critique parce qu'un autre thread l'utilise encore. Une fois qu'il a terminé, je peux finir de détruire l'objet. Est-ce que ça a du sens?

+6

Pourquoi détruisez-vous l'objet alors qu'il pourrait encore y avoir des threads qui y accèdent? –

+0

Legacy code, mon ami –

+0

Mes sympathies, alors :-) –

Répondre

3

En général, vous ne devez pas détruire un objet tant que vous ne savez pas qu'aucun autre thread ne l'utilise. Période.

Considérons ce scénario, en fonction de votre 'train de la pensée':

  • Discussion A: Récupère l'objet X référence
  • Discussion A: Objet de verrouillage X
  • Discussion B: Récupère l'objet X référence
  • discussion B: Bloc sur l'objet verrou X
  • discussion A: Déverrouillez objet X
  • Sujet B: Verouiller X; déverrouiller l'objet X; détruire un objet X

Considérons maintenant ce qui se passe si le timing est légèrement différent:

  • Discussion A: Récupère l'objet référence X
  • Discussion B: Récupère l'objet X référence
  • Discussion B: verrouillage objet X; déverrouiller l'objet X; détruire un objet X
  • Discussion A: Verouiller X - Crash

En bref, la destruction de l'objet doit être synchronisé quelque part autre que l'objet lui-même. Une option courante consiste à utiliser le comptage des références. Le thread A se verrouille sur la référence d'objet elle-même, empêchant la suppression de la référence et la destruction de l'objet, jusqu'à ce qu'il parvienne à incrémenter le nombre de références (en conservant l'objet en vie). Ensuite, le thread B efface simplement la référence et décrémente le compte de référence. Vous ne pouvez pas prédire quel thread appellera réellement le destructeur, mais il sera sûr de toute façon.

Le modèle de comptage de référence peut être implémenté facilement en utilisant boost::shared_ptr ou std::shared_ptr; le destructeur ne s'exécutera que si tous les threads ont été détruits (ou faits pour pointer ailleurs), donc au moment de la destruction, vous savez que le seul pointeur sur l'objet restant est le pointeur this du destructeur lui-même.

Notez que lorsque vous utilisez shared_ptr, il est important d'empêcher la modification de la référence d'objet d'origine jusqu'à ce que vous puissiez en capturer une copie. Par exemple:

std::shared_ptr<SomeObject> objref; 
Mutex objlock; 

void ok1() { 
    objlock.lock(); 
    objref->dosomething(); // ok; reference is locked 
    objlock.unlock(); 
} 

void ok2() { 
    std::shared_ptr<SomeObject> localref; 
    objlock.lock(); 
    localref = objref; 
    objlock.unlock(); 

    localref->dosomething(); // ok; local reference 
} 

void notok1() { 
    objref->dosomething(); // not ok; reference may be modified 
} 

void notok2() { 
    std::shared_ptr<SomeObject> localref = objref; // not ok; objref may be modified 
    localref->dosomething(); 
} 

Notez que simultanément lit sur un shared_ptr est sûr, vous pouvez choisir d'utiliser un verrou en lecture-écriture s'il est logique pour votre application.

+0

Bonne réponse! Toujours entendu parler de shared_ptr, je vais commencer à l'utiliser maintenant –

+0

Cette réponse a de bons conseils qui peuvent être utilisés dans certaines situations, mais qui sont généralement faux. "La destruction d'objet doit être synchronisée ailleurs que sur l'objet lui-même" - Ce n'est pas vrai. Un contre-exemple réfutant la règle est 'std :: condition_variable'; les clients sont spécifiquement autorisés à appeler le destructeur alors que les appels d'autres threads à wait() sont en cours. – ech

2

Si un objet est utilisé, vous devez vous assurer que le destructeur de l'objet n'est pas appelé avant la fin de l'utilisation de l'objet. Si tel est le comportement que vous avez alors son un problème potentiel et il doit vraiment être réparé.

Vous devez vous assurer que si un thread détruit vos objets, un autre thread ne devrait pas appeler des fonctions sur cet objet ou le premier thread devrait attendre que le second thread termine la fonction d'appel.

Oui, même les destructeurs peuvent nécessiter des sections critiques pour protéger la mise à jour de certaines données globales qui ne sont pas liées à la classe elle-même.

1

Il est possible que pendant qu'un thread attend CS dans destructor, l'autre détruise l'objet et si CS appartient à l'objet, il sera également détruit. Donc, ce n'est pas un bon design.

0

Vous avez absolument besoin de vous assurer que la durée de vie de votre objet est inférieure à celle des fils de consommation, ou vous avez des problèmes sérieux. Soit:

  1. Faire les consommateurs des enfants d'objet il est donc impossible pour eux d'exister en dehors de votre objet ou
  2. message d'utilisation passe/courtier.

Si vous allez cette dernière route, je recommande fortement 0mq http://www.zeromq.org/.

1

Oui, il est bien de le faire. Si une classe prend en charge une telle utilisation, les clients n'ont pas besoin de synchroniser la destruction; c'est-à-dire qu'ils n'ont pas besoin de s'assurer que toutes les autres méthodes sur l'objet ont terminé avant invoquant le destructeur.

Je recommanderais aux clients de ne pas supposer qu'ils peuvent le faire, sauf si cela est explicitement documenté. Les clients ont cette charge, par défaut, avec les objets de bibliothèque standard en particulier (§17.6.4.10/2).

Il y a des cas où c'est bien, cependant; Le destructeur std::condition_variable, par exemple, permet spécifiquement des invocations de méthodes condition_variable::wait() en cours lorsque ~condition_variable() commence. Il faut seulement que les clients ne lancent pas d'appels pour attendre() après~condition_variable() commence.

Il pourrait être plus propre d'exiger que le client synchronise l'accès au destructeur - et au constructeur d'ailleurs - comme le fait la plupart des autres bibliothèques standard. Je recommanderais de le faire si possible. Toutefois, il existe certains modèles dans lesquels il peut être judicieux de décharger les clients du fardeau de la synchronisation complète de la destruction. Le motif global de condition_variable semble être le même: envisager l'utilisation d'un objet qui gère des requêtes potentiellement longues. L'utilisateur effectue les opérations suivantes:

  1. construire l'objet
  2. Parce que l'objet à recevoir des demandes provenant d'autres fils.
  3. Cause que l'objet cesse de recevoir des demandes: à ce stade, certaines demandes en attente peuvent être en cours, mais aucune nouvelle ne peut être appelée.
  4. Détruisez l'objet. Le destructeur bloquera jusqu'à ce que toutes les demandes soient faites, sinon les demandes en cours pourraient avoir un mauvais temps.

Une alternative serait d'exiger que les clients aient besoin de synchroniser l'accès. Vous pouvez imaginer l'étape 3.5 ci-dessus où le client appelle une méthode shutdown() sur l'objet qui bloque, après quoi il est sûr que le client détruise l'objet. Cependant, cette conception a quelques inconvénients; il complique l'API et introduit un état supplémentaire pour l'objet de shutdown-but-valid. Envisagez plutôt de bloquer l'étape (3) jusqu'à ce que toutes les demandes soient effectuées. Il y a des compromis ...

+0

Quels critères suivriez-vous pour décider si le client devait synchroniser la destruction ou la synchroniser vous-même? –

+1

@dario_ramos - Les critères auxquels je peux penser sont: simplicité pour le client, par ex. des états supplémentaires à réfléchir, des fonctions supplémentaires à oublier etc .; simplicité pour le destructeur, c'est-à-dire qu'il est agréable s'il n'a pas à bloquer; et je pense que si tout le reste est égal, vous pourriez aussi bien faire que la bibliothèque standard ne le fait que pour la cohérence, c'est-à-dire exiger que le client se synchronise. D'autres? Donc - si vous pouvez éviter de bloquer dans le dtor sans imposer de fardeau au client, ne bloquez pas dans le destructeur, je suppose. – ech