244 votes

C# d'Événements et de la Sécurité des Threads

J'ai souvent entendre/lire les conseils suivants:

Toujours faire une copie d'un événement avant de le vérifier pour null et le feu. Cela permettra d'éliminer un problème potentiel avec filetage, où l'événement est d' null à l'emplacement de droite entre le point où vous vérifier la valeur null et où vous le feu de l'événement:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

Mise à jour: j'ai pensé à partir de la lecture sur les optimisations que cela pourrait également exiger de l'événement membre à être volatile, mais Jon Skeet unis dans sa réponse que le CLR n'optimisent pas loin de la copie.

Mais en attendant, pour que cette question de même, un autre thread doit avoir fait quelque chose comme ceci:

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...

La séquence réelle pourrait être ce mélange:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;    
// Good, now we can be certain that OnTheEvent will not run...

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

Le problème étant que l' OnTheEvent s'exécute après que l'auteur a désabonné, et pourtant ils ont juste désabonné en particulier pour éviter que cela se produise. Sûrement, ce qui est vraiment nécessaire est un événement personnalisé de mise en œuvre appropriée de la synchronisation dans l' add et remove accesseurs. Et en plus il y a le problème des blocages si un verrou est détenu alors qu'un événement est déclenché.

Est-ce donc le Culte du Cargo de la Programmation? Il semble de cette façon, beaucoup de gens doivent être de prendre cette mesure pour protéger leur code à partir de plusieurs threads, alors qu'en réalité, il me semble que les événements exigent beaucoup plus de soins que ce, avant qu'ils puissent être utilisés dans le cadre d'un multi-thread de conception. Par conséquent, les gens qui ne prennent pas que des soins supplémentaires pourrait tout aussi bien ignorer ce conseil - il n'est tout simplement pas un problème pour single-threaded programmes, et en fait, compte tenu de l'absence d' volatile dans la plupart des en ligne un exemple de code, le conseil a peut-être eu aucun effet du tout.

(Et n'est-il pas beaucoup plus simple pour attribuer le vide delegate { } sur la déclaration d'un membre de sorte que vous n'avez jamais besoin de vérifier pour null en premier lieu?)

Mise à jour: Dans le cas où c'était pas clair, je l'ai fait saisir l'intention du conseil - d'éviter une référence nulle exception, dans toutes les circonstances. Mon point c'est que cette exception référence nulle ne peut se produire si un autre thread est la radiation de l'événement, et la seule raison pour cela est de s'assurer que pas d'autres appels seront reçus par l'intermédiaire de l'événement, qui clairement n'est PAS obtenue par cette technique. Vous seriez cacher une condition de concurrence serait - il préférable de le révéler! Que nulle exception permet de détecter un abus de votre composant. Si vous souhaitez que votre composant à être protégés contre les abus, vous pouvez suivre l'exemple de WPF - stocker l'ID de thread dans votre constructeur et ensuite lancer une exception si un autre thread tente d'interagir directement avec votre composant. Ou encore de mettre en œuvre une véritable thread-safe composant (pas une tâche facile).

Donc je pense que le simple fait de faire ce copier/check idiome de culte du cargo de la programmation, de l'ajout de désordre et le bruit de votre code. Pour réellement protéger contre d'autres threads nécessite beaucoup plus de travail.

Mise à jour en réponse à Eric Lippert du blog:

Donc, il y a une chose importante que j'avais raté sur les gestionnaires d'événements: "les gestionnaires d'événements sont nécessaires pour être robuste en face de l'être, même après que l'événement a été désabonné", et, évidemment, donc nous avons seulement besoin de se soucier de la possibilité de l'événement délégué null. Cette exigence sur les gestionnaires d'événements documentés n'importe où?

Et donc: "Il y a d'autres façons de résoudre ce problème; par exemple, l'initialisation du gestionnaire d'avoir un vide d'action qui n'est jamais supprimé. Mais ce faisant, null vérifier est que le modèle standard."

Donc le restant un fragment de ma question est, pourquoi est explicite-null-cocher la case "modèle standard"? L'alternative, d'attribuer le vide délégué, ne nécessite qu' = delegate {} à être ajouté à l'événement de la déclaration, et cela élimine ces petits tas de stinky cérémonie de chaque lieu où l'événement est déclenché. Il serait facile de assurez-vous que le vide d'un délégué n'est pas cher pour instancier. Ou suis-je manque encore quelque chose?

Certes, il faut que (comme Jon Skeet suggéré) ce qui est juste .NET 1.x conseil qui n'a pas disparu, comme il aurait dû le faire en 2005?

103voto

Jon Skeet Points 692016

L'équipe n'est pas autorisé à effectuer l'optimisation dont vous parlez dans la première partie, en raison de l'état. Je sais que cela a été évoqué comme un spectre il y a un moment, mais il n'est pas valide. (J'ai vérifié avec Joe Duffy ou Vance Morrison il y a un moment; je ne me souviens pas où.)

Sans la volatilité de modificateur, il est possible que la copie locale seront pas à jour, mais c'est tout. Il ne cause pas un NullReferenceException.

Et oui, il est certainement une condition de concurrence - mais il y en aura toujours. Supposons que nous venons de changer le code pour:

TheEvent(this, EventArgs.Empty);

Supposons maintenant que la liste d'invocation pour que délégué a 1000 entrées. Il est parfaitement possible que l'action au début de la liste aura exécuté avant qu'un autre thread désabonner à un gestionnaire près de la fin de la liste. Cependant, ce gestionnaire sera toujours exécuté, car il y aura une nouvelle liste. (Les délégués sont immuables.) Aussi loin que je peux voir, c'est inévitable.

À l'aide d'un vide délégué certainement évite la nullité de vérifier, mais ne résout pas la condition de la course. Il ne garantit pas que vous toujours "voir" la dernière valeur de la variable.

82voto

Eric Lippert Points 300275

Cette question s'est posée dans une discussion interne d'un moment de retour; j'ai l'intention de ce blog depuis quelques temps maintenant.

Mon post sur le sujet est ici:

Des événements et des compétitions

52voto

JP Alioto Points 33482

Je vois beaucoup de gens qui vont vers l'extension de la méthode de le faire ...

public static class Extensions   
{   
  public static void Raise<T>(this EventHandler<T> handler, 
    object sender, T args) where T : EventArgs   
  {   
    if (handler != null) handler(sender, args);   
  }   
}

Qui vous donne plus agréable syntaxe pour déclencher l'événement ...

MyEvent.Raise( this, new MyEventArgs() );

Et le fait aussi loin avec la copie locale, car il est capturé à l'appel de la méthode du temps.

36voto

Daniel Fortunov Points 12044

"Pourquoi est-explicite-null-cochez la case "modèle standard'?"

Je soupçonne que la raison pour cela peut être que le null-case est plus performant.

Si vous avez toujours souscrire un vide délégué à vos événements lorsqu'ils sont créés, il y aura quelques frais:

  • Coût de construction du vide délégué.
  • Coût de construction d'un délégué de la chaîne de la contenir.
  • Coût de l'invocation de l'inutile délégué à chaque fois que l'événement est déclenché.

(Notez que les contrôles de l'INTERFACE utilisateur ont souvent un grand nombre d'événements, dont la plupart ne sont jamais souscrit. Avoir à créer un mannequin abonné pour chaque événement, puis invoque, il serait probablement un important gain de performance.)

J'ai fait quelques superficielle de la performance des tests pour voir l'impact de la abonnez-vide-délégué approche, et voici mes résultats:

Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      432ms
OnClassicNullCheckedEvent took: 490ms
OnPreInitializedEvent took:     614ms <--
Subscribing an empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      674ms
OnClassicNullCheckedEvent took: 674ms
OnPreInitializedEvent took:     2041ms <--
Subscribing another empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      2011ms
OnClassicNullCheckedEvent took: 2061ms
OnPreInitializedEvent took:     2246ms <--
Done

À noter que pour le cas de zéro ou un nombre d'abonnés (commun pour les contrôles de l'INTERFACE utilisateur, où les événements sont nombreux), l'événement pré-initialisée avec un vide délégué est notamment plus lent (plus de 50 millions d'itérations...)

Pour plus d'informations et le code source, consultez cet article de blog sur .NET en Cas d'invocation de la sécurité des threads que j'ai publié juste le jour avant cette question a été posée (!)

(Mon test set-up peut être imparfaite, donc n'hésitez pas à télécharger le code source et l'inspecter vous-même. Tout commentaire est apprécié.)

12voto

Chuck Savage Points 6106

J'ai vraiment apprécié cette lecture ne pas! Même si j'en ai besoin pour travailler avec le C# fonction appelée événements!

Pourquoi ne pas résoudre ce problème dans le compilateur? Je sais qu'il y a MME des gens qui lisent ces messages, merci donc de ne pas la flamme!

1 - le problème de Null) Pourquoi ne pas faire les événements .Vide au lieu de null en premier lieu? Combien de lignes de code serait sauvé null vérifier ou d'avoir à coller un = delegate {} sur la déclaration? Laisser le compilateur gérer le caisson Vide, c'est à dire ne rien faire! Si toutes les questions au créateur de l'événement, ils peuvent vérifier .Vide et faire ce qu'ils soins avec elle! Sinon, toutes les nuls contrôles / délégué ajoute sont des hacks de contourner le problème!

Honnêtement, je suis fatigué d'avoir à faire avec chaque événement - aka code réutilisable!

public event Action<thisClass, string> Some;
protected virtual void DoSomeEvent(string someValue)
{
  var e = Some; // avoid race condition here! 
  if(null != e) // avoid null condition here! 
     e(this, someValue);
}

2 - la condition de la course question) j'ai lu Eric du blog, je suis d'accord que le H (handler) doit gérer quand il déréférence lui-même, mais ne peuvent pas le cas mises à la immuables/thread-safe? C'est à dire, définir un verrouillage de l'indicateur lors de sa création, de sorte que chaque fois qu'il est appelé, il verrouille tous les signataires et de l'onu-abonnant à elle, tandis que son exécution?

Conclusion,

Ne sont pas de jour moderne langues censé résoudre les problèmes de ce genre pour nous?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X