164 votes

Que fait le SynchronizationContext ?

Dans le livre "Programming C#", il y a un exemple de code qui concerne SynchronizationContext :

SynchronizationContext originalContext = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(delegate {
    string text = File.ReadAllText(@"c:\temp\log.txt");
    originalContext.Post(delegate {
        myTextBox.Text = text;
    }, null);
});

Je suis un débutant en matière de fils, alors veuillez répondre en détail. Tout d'abord, je ne sais pas ce que signifie le contexte, ce que le programme sauvegarde dans le originalContext ? Et quand le Post est déclenchée, que va faire le thread de l'interface utilisateur ?
Si je pose des questions stupides, merci de me corriger, merci !

EDIT : Par exemple, que se passe-t-il si j'écris simplement myTextBox.Text = text; dans la méthode, quelle est la différence ?

206voto

stakx Points 29832

Que fait le SynchronizationContext ?

C'est simple, SynchronizationContext représente un emplacement "où" le code peut être exécuté. Les délégués qui sont passés à son Send o Post méthode sera alors invoqué à cet endroit. ( Post est la version non-bloquante / asynchrone de Send .)

Chaque fil peut avoir un SynchronizationContext instance qui lui est associée. Le thread en cours d'exécution peut être associé à un contexte de synchronisation en appelant la fonction statique SynchronizationContext.SetSynchronizationContext méthode et le contexte actuel du thread en cours d'exécution peut être interrogé via la fonction SynchronizationContext.Current propriété .

Malgré ce que je viens d'écrire (chaque thread ayant un contexte de synchronisation associé), une SynchronizationContext ne représente pas nécessairement un fil conducteur il peut également transmettre l'invocation des délégués qui lui sont passés à l'adresse suivante l'un des nombreux (par exemple, vers un ThreadPool de travail), ou (du moins en théorie) à un processus spécifique de Noyau du CPU ou même à un autre hôte du réseau . L'endroit où vos délégués finissent par courir dépend du type d'entreprise. SynchronizationContext utilisé.

Windows Forms installera un WindowsFormsSynchronizationContext sur le fil de discussion sur lequel le premier formulaire est créé. (Ce thread est communément appelé "le thread de l'interface utilisateur".) Ce type de contexte de synchronisation invoque les délégués qui lui sont transmis sur ce thread exactement. Ceci est très utile car Windows Forms, comme beaucoup d'autres cadres d'interface utilisateur, ne permet la manipulation des contrôles que sur le même thread que celui sur lequel ils ont été créés.

Et si j'écrivais juste myTextBox.Text = text; dans la méthode, quelle est la différence ?

Le code que vous avez passé à ThreadPool.QueueUserWorkItem sera exécuté sur un thread de travail du pool de threads. C'est-à-dire qu'il ne sera pas exécuté sur le thread sur lequel se trouve votre myTextBox a été créé, de sorte que Windows Forms lèvera tôt ou tard (surtout dans les versions Release) une exception, vous indiquant que vous ne pouvez pas accéder à myTextBox d'un autre fil.

C'est la raison pour laquelle vous devez, d'une manière ou d'une autre, "repasser" du fil de travail au "fil de l'interface utilisateur" (où myTextBox a été créé) avant cette affectation particulière. Cette opération s'effectue comme suit :

  1. Pendant que vous êtes encore sur le fil de l'interface utilisateur, capturez le fil de Windows Forms SynchronizationContext et stocke une référence à celle-ci dans une variable ( originalContext ) pour une utilisation ultérieure. Vous devez interroger SynchronizationContext.Current à ce stade ; si vous l'interrogez à l'intérieur du code transmis à ThreadPool.QueueUserWorkItem vous pouvez obtenir le contexte de synchronisation associé au thread de travail du pool de threads. Une fois que vous avez stocké une référence au contexte de Windows Forms, vous pouvez l'utiliser n'importe où et à tout moment pour "envoyer" du code au thread de l'interface utilisateur.

  2. Chaque fois que vous devez manipuler un élément de l'interface utilisateur (mais que vous n'êtes pas, ou pourriez ne plus être, sur le thread de l'interface utilisateur), accédez au contexte de synchronisation de Windows Forms via originalContext et confier le code qui manipulera l'interface utilisateur à l'une des deux personnes suivantes Send o Post .


Remarques finales et conseils :

  • Quels contextes de synchronisation ne le fera pas vous indique quel code doit être exécuté dans un emplacement/contexte spécifique, et quel code peut être exécuté normalement, sans le passer à un fichier de type SynchronizationContext . Pour en décider, vous devez connaître les règles et les exigences du cadre dans lequel vous programmez - Windows Forms dans ce cas.

    Retenez donc cette règle simple pour les formulaires Windows : NE PAS accéder aux contrôles ou aux formulaires à partir d'un fil de discussion autre que celui qui les a créés. Si vous devez le faire, utilisez la fonction SynchronizationContext comme décrit ci-dessus, ou Control.BeginInvoke (qui est une façon spécifique à Windows Forms de faire exactement la même chose).

  • Si vous programmez avec .NET 4.5 ou une version ultérieure, vous pouvez vous simplifier la vie en convertissant le code qui utilise explicitement l'option SynchronizationContext , ThreadPool.QueueUserWorkItem , control.BeginInvoke etc. vers la nouvelle async / await mots-clés et le Bibliothèque parallèle aux tâches (TPL) c'est-à-dire l'API qui entoure l Task y Task<TResult> classes. Celles-ci se chargeront, dans une très large mesure, de capturer le contexte de synchronisation du thread de l'interface utilisateur, de lancer une opération asynchrone, puis de revenir sur le thread de l'interface utilisateur afin de pouvoir traiter le résultat de l'opération.

27voto

Noseratio Points 23840

J'aimerais compléter les autres réponses, SynchronizationContext.Post met simplement en file d'attente un callback pour une exécution ultérieure sur le thread cible (normalement pendant le prochain cycle de la boucle de messages du thread cible), puis l'exécution continue sur le thread appelant. D'un autre côté, SynchronizationContext.Send essaie d'exécuter le callback sur le thread cible immédiatement, ce qui bloque le thread appelant et peut entraîner un blocage. Dans les deux cas, il existe une possibilité de réentrance du code (entrée d'une méthode de classe sur le même thread d'exécution avant que l'appel précédent à la même méthode ne soit revenu).

Si vous êtes familier avec le modèle de programmation Win32, une analogie très proche serait PostMessage y SendMessage que vous pouvez appeler pour envoyer un message à partir d'un thread différent de celui de la fenêtre cible.

Voici une très bonne explication de ce que sont les contextes de synchronisation : Tout est dans le SynchronizationContext .

20voto

Hans Passant Points 475940

Il stocke le fournisseur de synchronisation, une classe dérivée de SynchronizationContext. Dans ce cas, ce sera probablement une instance de WindowsFormsSynchronizationContext. Cette classe utilise les méthodes Control.Invoke() et Control.BeginInvoke() pour implémenter les méthodes Send() et Post(). Ou bien il peut s'agir de DispatcherSynchronizationContext, elle utilise alors Dispatcher.Invoke() et BeginInvoke(). Dans une application Winforms ou WPF, ce fournisseur est automatiquement installé dès que vous créez une fenêtre.

Lorsque vous exécutez du code sur un autre thread, comme le thread-pool utilisé dans le snippet, vous devez faire attention à ne pas utiliser directement des objets qui ne sont pas sûrs pour les threads. Comme tout objet d'interface utilisateur, vous devez mettre à jour la propriété TextBox.Text à partir du thread qui a créé le TextBox. La méthode Post() garantit que la cible du délégué s'exécute sur ce thread.

Attention, ce snippet est un peu dangereux, il ne fonctionnera correctement que si vous l'appelez depuis le thread UI. SynchronizationContext.Current a des valeurs différentes dans les différents threads. Seul le thread UI a une valeur utilisable. C'est la raison pour laquelle le code a dû la copier. Une façon plus lisible et plus sûre de le faire, dans une application Winforms :

    ThreadPool.QueueUserWorkItem(delegate {
        string text = File.ReadAllText(@"c:\temp\log.txt");
        myTextBox.BeginInvoke(new Action(() => {
            myTextBox.Text = text;
        }));
    });

Ce qui a l'avantage de fonctionner lorsqu'il est appelé à partir de tout fil. L'avantage d'utiliser SynchronizationContext.Current est que cela fonctionne toujours, que le code soit utilisé dans Winforms ou WPF, peu importe dans une bibliothèque. C'est certainement no un bon exemple d'un tel code, vous savez toujours quel type de TextBox vous avez ici, donc vous savez toujours s'il faut utiliser Control.BeginInvoke ou Dispatcher.BeginInvoke. En fait, l'utilisation de SynchronizationContext.Current n'est pas si courante.

Le livre essaie de vous enseigner l'enfilage, donc l'utilisation de cet exemple imparfait est acceptable. Dans la vie réelle, dans les quelques cas où vous puede Si vous envisagez d'utiliser SynchronizationContext.Current, vous laisserez toujours aux mots-clés async/await de C# ou à TaskScheduler.FromCurrentSynchronizationContext() le soin de le faire pour vous. Mais notez qu'ils se comportent toujours mal, comme le fait le snippet, lorsque vous les utilisez sur le mauvais thread, pour la même raison. Une question très fréquente par ici, le niveau d'abstraction supplémentaire est utile mais rend plus difficile de comprendre pourquoi ils ne fonctionnent pas correctement. Espérons que le livre vous indique également quand ne pas l'utiliser :)

5voto

Erik Funkenbusch Points 53436

Le but du contexte de synchronisation ici est de s'assurer que myTextbox.Text = text; est appelé sur le thread principal de l'interface utilisateur.

Windows exige que les contrôles de l'interface graphique ne soient accessibles que par le thread avec lequel ils ont été créés. Si vous essayez d'assigner le texte dans un thread d'arrière-plan sans synchronisation préalable (par l'un des moyens suivants, comme ceci ou le motif Invoke), une exception sera levée.

Il s'agit de sauvegarder le contexte de synchronisation avant de créer le fil d'arrière-plan, puis le fil d'arrière-plan utilise la méthode context.Post pour exécuter le code de l'interface graphique.

Oui, le code que vous avez montré est fondamentalement inutile. Pourquoi créer un thread d'arrière-plan, pour avoir immédiatement besoin de revenir au thread principal de l'interface utilisateur ? C'est juste un exemple.

4voto

Bigeyes Points 495

À la source

Chaque thread a un contexte qui lui est associé - c'est ce qu'on appelle le contexte "courant" - et ces contextes peuvent être partagés entre plusieurs threads. L'ExecutionContext contient les métadonnées pertinentes de l'environnement ou du contexte actuel dans lequel le programme est en cours d'exécution. Le SynchronizationContext représente une abstraction -- il désigne l'endroit où le code de votre application est exécuté.

Un SynchronizationContext permet de mettre une tâche en file d'attente sur un autre contexte. Notez que chaque thread peut avoir son propre SynchronizatonContext.

Par exemple : Supposons que vous ayez deux threads, Thread1 et Thread2. Disons que Thread1 est en train d'effectuer un travail, et qu'ensuite Thread1 souhaite exécuter du code sur Thread2. Une façon possible de le faire est de demander à Thread2 son objet SynchronizationContext, de le donner à Thread1, et ensuite Thread1 peut appeler SynchronizationContext.Send pour exécuter le code sur Thread2.

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