Que fait SynchronizationContext?

Dans le livre Programming C #, il contient un exemple de code relatif à SynchronizationContext :

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

Je suis un débutant dans les discussions, alors s’il vous plaît répondez en détail. Premièrement, je ne sais pas ce que signifie le contexte, que sauvegarde-t-il dans originalContext ? Et quand la méthode Post est lancée, que fera le thread UI?
Si je demande des bêtises, corrigez-moi, merci!

EDIT: Par exemple, que faire si j’écris simplement myTextBox.Text = text; dans la méthode, quelle est la différence?

Que fait SynchronizationContext?

En termes simples, SynchronizationContext représente un emplacement “où” du code peut être exécuté. Les delegates passés à sa méthode Send ou Post seront alors appelés à cet emplacement. ( Post est la version non bloquante / asynchrone de Send .)

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

Malgré ce que je viens d’écrire (chaque thread ayant un contexte de synchronisation associé), un SynchronizationContext ne représente pas nécessairement un thread spécifique ; Il peut également transférer l’invocation des delegates qui lui sont passés vers plusieurs threads (par exemple vers un ThreadPool travail ThreadPool ), ou (au moins en théorie) vers un cœur de processeur spécifique, ou même vers un autre hôte de réseau . L’endroit où vos delegates s’exécutent dépend du type de SynchronizationContext utilisé.

Windows Forms installera un WindowsFormsSynchronizationContext sur le thread sur lequel le premier formulaire est créé. (Ce thread est communément appelé “le thread d’interface utilisateur”.) Ce type de contexte de synchronisation appelle les delegates qui lui sont passés sur exactement ce thread. Ceci est très utile car Windows Forms, comme de nombreux autres frameworks d’interface utilisateur, autorise uniquement la manipulation des contrôles sur le même thread sur lequel ils ont été créés.

Et si myTextBox.Text = text; simplement myTextBox.Text = text; dans la méthode, quelle est la différence?

Le code que vous avez transmis à ThreadPool.QueueUserWorkItem sera exécuté sur un thread de travail du pool de threads. En d’autres myTextBox , il ne s’exécutera pas sur le thread sur lequel votre myTextBox été créé. Windows Forms lancera tôt ou tard (en particulier dans les versions Release) une exception vous indiquant que vous ne pouvez pas accéder à myTextBox un autre thread.

C’est pourquoi vous devez en quelque sorte “revenir” du thread de travail au “thread d’interface utilisateur” (où myTextBox été créé) avant cette affectation particulière. Cela se fait comme suit:

  1. Pendant que vous êtes toujours sur le thread d’interface utilisateur, capturez-y le SynchronizationContext Windows Forms et stockez-y une référence dans une variable ( originalContext ) pour une utilisation ultérieure. Vous devez interroger SynchronizationContext.Current à ce stade; Si vous l’avez interrogé dans le 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 d’interface utilisateur.

  2. Chaque fois que vous devez manipuler un élément d’interface utilisateur (mais ne figure plus ou ne figure plus sur le thread d’interface utilisateur), accédez au contexte de synchronisation de Windows Forms via originalContext et transmettez le code qui manipulera l’interface utilisateur à Send ou Post .


Remarques finales et conseils:

  • Ce que les contextes de synchronisation ne feront pas pour vous, c’est de vous dire quel code doit être exécuté dans un emplacement / contexte spécifique et quel code peut simplement être exécuté normalement, sans le transmettre à un SynchronizationContext . Pour décider de cela, vous devez connaître les règles et les exigences du framework que vous programmez – Windows Forms dans ce cas.

    Donc, rappelez-vous cette règle simple pour Windows Forms: n’accédez PAS aux contrôles ou aux formulaires à partir d’un thread autre que celui qui les a créés. Si vous devez faire cela, utilisez le mécanisme SynchronizationContext comme décrit ci-dessus ou Control.BeginInvoke (qui est un moyen spécifique à Windows Forms de faire exactement la même chose).

  • Si vous programmez avec .NET 4.5 ou version ultérieure, vous pouvez vous simplifier la vie en convertissant votre code qui utilise explicitement SynchronizationContext , ThreadPool.QueueUserWorkItem , control.BeginInvoke , etc., en nouveaux mots – clés async / en await et en parallèle à la tâche. Library (TPL) , c’est-à-dire l’API entourant les classes Task et Task . Celles-ci prendront très en charge la capture du contexte de synchronisation du thread d’interface utilisateur, le démarrage d’une opération asynchrone, puis le retour sur le thread d’interface utilisateur afin de pouvoir traiter le résultat de l’opération.

Je voudrais append d’autres réponses, SynchronizationContext.Post ne fait que mettre en attente un rappel pour une exécution ultérieure sur le thread cible (normalement pendant le cycle suivant de la boucle de message du thread cible), puis l’exécution continue sur le thread appelant. D’autre part, SynchronizationContext.Send essaie d’exécuter immédiatement le rappel sur le thread cible, 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 renvoyé).

Si vous êtes familier avec le modèle de programmation Win32, une analogie très proche serait les API PostMessage et 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: C’est tout sur le SynchronizationContext .

Il stocke le fournisseur de synchronisation, une classe dérivée de SynchronizationContext. Dans ce cas, il s’agira probablement d’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 il peut s’agir de DispatcherSynchronizationContext, il utilise 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 du pool de threads utilisé dans l’extrait de code, vous devez vous assurer que vous n’utilisez pas directement des objects non sécurisés pour les threads. Comme tout object d’interface utilisateur, vous devez mettre à jour la propriété TextBox.Text à partir du thread qui a créé la zone de texte. La méthode Post () garantit que la cible du délégué s’exécute sur ce thread.

Attention, cet extrait de code est un peu dangereux, il ne fonctionnera correctement que lorsque vous l’appelez à partir du thread d’interface utilisateur. SynchronizationContext.Current a des valeurs différentes dans différents threads. Seul le thread d’interface utilisateur a une valeur utilisable. Et c’est la raison pour laquelle le code a dû le copier. Un moyen plus lisible et plus sûr de le faire, dans une application Winforms:

  ThreadPool.QueueUserWorkItem(delegate { ssortingng 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é depuis n’importe quel thread. L’avantage de l’utilisation de SynchronizationContext.Current est qu’il fonctionne toujours, que le code soit utilisé dans Winforms ou WPF, cela compte dans une bibliothèque. Ce n’est certainement pas un bon exemple d’un tel code, vous savez toujours quel type de zone de texte vous avez ici, donc vous savez toujours s’il faut utiliser Control.BeginInvoke ou Dispatcher.BeginInvoke. En fait, en utilisant SynchronizationContext.Current n’est pas si courant.

Le livre essaie de vous apprendre à propos du threading, donc l’utilisation de cet exemple imparfait est okayish. Dans la vraie vie, dans les quelques cas où vous pourriez envisager d’utiliser SynchronizationContext.Current, vous devriez toujours laisser les mots clés asynchrones / wait de C # ou TaskScheduler.FromCurrentSynchronizationContext () le faire pour vous. Mais notez qu’ils se comportent encore mal comme le fait l’extrait de code lorsque vous les utilisez sur le mauvais thread, pour la même raison. Une question très courante ici, le niveau d’abstraction supplémentaire est utile, mais rend plus difficile de comprendre pourquoi elles ne fonctionnent pas correctement. Espérons que le livre vous indique également quand ne pas l’utiliser 🙂

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

Windows exige que les contrôles graphiques soient accessibles uniquement par le thread avec lequel ils ont été créés. Si vous essayez d’affecter le texte dans un thread d’arrière-plan sans synchroniser au préalable (via l’un des moyens, tels que celui-ci ou le motif Invoke), une exception sera lancée.

Cela permet de sauvegarder le contexte de synchronisation avant de créer le thread d’arrière-plan, puis le thread d’arrière-plan utilise la méthode context.Post pour exécuter le code GUI.

Oui, le code que vous avez montré est fondamentalement inutile. Pourquoi créer un thread d’arrière-plan, seulement pour retourner immédiatement au thread d’interface utilisateur principal? C’est juste un exemple.

Vous et votre femme expédiez des cadeaux séparés pour des personnes séparées. Vous envoyez pour votre père et elle envoie sa mère. Vous préparez vos colis et les mettez dans le porche. (Thread 0) Vous voulez qu’il soit livré via Fedex (Thread 1) et qu’elle souhaite via UPS (Thread 2). Vous attendez tous les deux des avis de livraison de la même personne à votre domicile. (contexte de synchronisation). Il saisit les paquets et les envoie via Fedex et UPS. Finalement, cette personne ne doit pas envoyer d’avis à votre adresse professionnelle, car il ne peut pas entrer dans le bâtiment (violation d’access, il doit retourner d’où il est appelé). Ainsi, une fois que vos colis sont livrés, il retourne à votre domicile et quitte la notification, les colis ont été livrés.

Le partage de ressources entre le thread 1 et le thread 2 est une analogie plus complexe. Le scénario ci-dessus est l’utilisation la plus élémentaire dans les appels réseau.

À la source

Un contexte est associé à chaque thread (également appelé contexte “actuel”) et ces contextes peuvent être partagés entre les threads. Le fichier d’exécution contient des 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 indique l’emplacement où le code de votre application est exécuté.

Un SynchronizationContext vous permet de mettre en queue une tâche dans 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 fait du travail, puis Thread1 souhaite exécuter du code sur Thread2. Une manière possible de le faire est de demander à Thread2 son object SynchronizationContext, de le donner à Thread1, puis Thread1 peut appeler SynchronizationContext.Send pour exécuter le code sur Thread2.

Cet exemple provient des exemples de Linqpad de Joseph Albahari, mais il aide vraiment à comprendre ce que fait le contexte de synchronisation.

 void WaitForTwoSecondsAsync (Action continuation) { continuation.Dump(); var syncContext = AsyncOperationManager.SynchronizationContext; new Timer (_ => syncContext.Post (o => continuation(), _)).Change (2000, -1); } void Main() { Util.CreateSynchronizationContext(); ("Waiting on thread " + Thread.CurrentThread.ManagedThreadId).Dump(); for (int i = 0; i < 10; i++) WaitForTwoSecondsAsync (() => ("Done on thread " + Thread.CurrentThread.ManagedThreadId).Dump()); } 

SynchronizationContext nous permet de mettre à jour une interface utilisateur à partir d’un thread différent (de manière synchrone via la méthode Send ou de manière asynchrone via la méthode Post).

Jetez un oeil à l’exemple suivant:

  private void SynchronizationContext SyncContext = SynchronizationContext.Current; private void Button_Click(object sender, RoutedEventArgs e) { Thread thread = new Thread(Work1); thread.Start(SyncContext); } private void Work1(object state) { SynchronizationContext syncContext = state as SynchronizationContext; syncContext.Post(UpdateTextBox, syncContext); } private void UpdateTextBox(object state) { Thread.Sleep(1000); ssortingng text = File.ReadAllText(@"c:\temp\log.txt"); myTextBox.Text = text; } 

SynchronizationContext.Current renvoie le contexte de synchronisation du thread d’interface utilisateur. Comment puis-je le savoir? Au début de chaque formulaire ou application WPF, le contexte sera défini sur le thread d’interface utilisateur. Si vous créez une application WPF et exécutez mon exemple, vous verrez que lorsque vous cliquez sur le bouton, il dort environ 1 seconde, puis affiche le contenu du fichier. Vous pourriez vous attendre à ce que ce ne soit pas le cas parce que l’appelant de la méthode UpdateTextBox (qui est Work1) est une méthode transmise à un thread, par conséquent, il ne doit pas utiliser le thread d’interface utilisateur principal, NOPE! Même si la méthode Work1 est transmise à un thread, notez qu’elle accepte également un object qui est le SyncContext. Si vous le regardez, vous verrez que la méthode UpdateTextBox est exécutée via la méthode syncContext.Post et non la méthode Work1. Jetez un oeil à ce qui suit:

 private void Button_Click(object sender, RoutedEventArgs e) { Thread.Sleep(1000); ssortingng text = File.ReadAllText(@"c:\temp\log.txt"); myTextBox.Text = text; } 

Le dernier exemple et celui-ci s’exécute de la même manière. Les deux ne bloquent pas l’interface utilisateur pendant qu’elle le fait.

En conclusion, pensez à SynchronizationContext en tant que thread. Ce n’est pas un thread, il définit un thread (notez que tous les threads n’ont pas un SyncContext). Chaque fois que nous appelons la méthode Post ou Send pour mettre à jour une interface utilisateur, cela revient à mettre à jour l’interface utilisateur normalement à partir du thread d’interface utilisateur principal. Si, pour certaines raisons, vous devez mettre à jour l’interface utilisateur à partir d’un thread différent, assurez-vous que le thread possède le SyncContext du thread d’interface utilisateur principal et appelez simplement la méthode Send ou Post avec la méthode que vous souhaitez exécuter. ensemble.

J’espère que cela vous aide, mon pote!