Que fait SynchronizationContext?

135

Dans le livre Programming C #, il contient un exemple de code sur 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 dans les discussions, alors veuillez répondre en détail. Premièrement, je ne sais pas ce que signifie le contexte, qu'est-ce que le programme enregistre dans le originalContext? Et lorsque la Postméthode est déclenchée, que fera le thread d'interface utilisateur?
Si je demande des choses idiotes, veuillez 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?

nuageuxFan
la source
1
Le manuel précis
ta.speot.is
IMHO async
wait
7
@RoyiNamir: Oui, mais devinez quoi: async/ awaits'appuie sur en SynchronizationContextdessous.
stakx - ne contribue plus

Réponses:

170

Que fait SynchronizationContext?

En termes simples, SynchronizationContextreprésente un emplacement «où» le code pourrait être exécuté. Les délégués qui sont passés à sa méthodeSend ou seront alors appelés à cet emplacement. ( est la version non bloquante / asynchrone de .)PostPostSend

Chaque thread peut être SynchronizationContextassocié à une instance. Le thread en cours d'exécution peut être associé à un contexte de synchronisation en appelant la méthode statiqueSynchronizationContext.SetSynchronizationContext , et le contexte actuel du thread en cours d'exécution peut être interrogé via la SynchronizationContext.Currentpropriété .

Malgré ce que je viens d'écrire (chaque thread ayant un contexte de synchronisation associé), a SynchronizationContextne représente pas nécessairement un thread spécifique ; il peut également transmettre l'invocation des délégués qui lui sont passés à l' un de plusieurs threads (par exemple à un ThreadPoolthread de travail), ou (au moins en théorie) à un cœur de processeur spécifique , 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 de serveur SynchronizationContextutilisé.

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 délégués qui lui sont passés exactement sur ce thread. Ceci est très utile car Windows Forms, comme de nombreux autres frameworks d'interface utilisateur, permet uniquement la manipulation des contrôles sur le même thread sur lequel ils ont été créés.

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

Le code que vous avez transmis ThreadPool.QueueUserWorkItemsera exécuté sur un thread de travail de pool de threads. Autrement dit, il ne s'exécutera pas sur le thread sur lequel votre a myTextBoxété créé, donc Windows Forms lèvera tôt ou tard (en particulier dans les versions Release) une exception, vous indiquant que vous ne pouvez pas accéder à myTextBoxpartir d'un autre thread.

C'est pourquoi vous devez en quelque sorte «revenir» du thread de travail au «thread d'interface utilisateur» (où a 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 Windows Forms SynchronizationContextet stockez une référence à celui-ci dans une variable ( originalContext) pour une utilisation ultérieure. Vous devez interroger SynchronizationContext.Currentà ce stade; si vous l'avez interrogé dans le code passé à 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 avez besoin de manipuler un élément de l'interface utilisateur (mais que vous n'êtes plus, ou peut-être plus, sur le thread de l'interface utilisateur), accédez au contexte de synchronisation de Windows Forms via originalContextet transférez le code qui manipulera l'interface utilisateur à Sendou Post.


Remarques finales et conseils:

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

    Rappelez-vous donc cette règle simple pour Windows Forms: N'accédez PAS aux contrôles ou formulaires à partir d'un thread autre que celui qui les a créés. Si vous devez le faire, utilisez le SynchronizationContextmécanisme décrit ci-dessus, ou Control.BeginInvoke(qui est une manière spécifique à Windows Forms de faire exactement la même chose).

  • Si vous programmons contre .NET 4.5 ou version ultérieure, vous pouvez vous rendre la vie beaucoup plus facile en convertissant votre code explicitement usages SynchronizationContext, ThreadPool.QueueUserWorkItem, control.BeginInvoke, etc. vers les nouveaux async/ awaitmots - clés et la bibliothèque parallèle de tâches (TPL) , à savoir l'API entourant les classes Tasket Task<TResult>. Ceux-ci, dans une très large mesure, se chargeront de capturer le contexte de synchronisation du thread d'interface utilisateur, de démarrer une opération asynchrone, puis de revenir sur le thread d'interface utilisateur afin que vous puissiez traiter le résultat de l'opération.

stakx - ne contribue plus
la source
Vous dites que Windows Forms, comme beaucoup d'autres frameworks d'interface utilisateur, autorise uniquement la manipulation des contrôles sur le même thread, mais toutes les fenêtres de Windows doivent être accessibles par le même thread qui l'a créé.
user34660
4
@ user34660: Non, ce n'est pas correct. Vous pouvez avoir plusieurs threads qui créent des contrôles Windows Forms. Mais chaque contrôle est associé au thread qui l'a créé et ne doit être accédé que par ce thread. Les contrôles de différents threads d'interface utilisateur sont également très limités dans la façon dont ils interagissent les uns avec les autres: l'un ne peut pas être le parent / enfant de l'autre, la liaison de données entre eux n'est pas possible, etc. Enfin, chaque thread qui crée des contrôles a besoin de son propre message loop (qui démarre par Application.Run, IIRC). C'est un sujet assez avancé et non quelque chose de désinvolte.
stakx - ne contribue plus le
Mon premier commentaire est dû au fait que vous disiez "comme beaucoup d'autres frameworks d'interface utilisateur", ce qui implique que certaines fenêtres autorisent la "manipulation des contrôles" à partir d'un thread différent, mais aucune fenêtre Windows ne le fait. Vous ne pouvez pas «avoir plusieurs threads qui créent des contrôles Windows Forms» pour la même fenêtre et «doivent être accédés par le même thread» et «ne doit être accédé que par ce thread» disent la même chose. Je doute qu'il soit possible de créer des "Contrôles à partir de différents threads d'interface utilisateur" pour la même fenêtre. Tout cela n'est pas avancé pour ceux d'entre nous qui ont déjà utilisé la programmation Windows avant .Net.
user34660
3
Tout ce discours sur les «fenêtres» et les «fenêtres Windows» me donne un peu le vertige. Ai-je mentionné l'une de ces «fenêtres»? Je ne pense pas ...
stakx - ne contribue plus
1
@ibubi: Je ne suis pas sûr de comprendre votre question. Le contexte de synchronisation de n'importe quel thread n'est pas défini ( null) ou une instance de SynchronizationContext(ou une sous-classe de celui-ci). Le but de cette citation n'était pas ce que vous obtenez, mais ce que vous n'obtiendrez pas : le contexte de synchronisation du thread d'interface utilisateur.
stakx - ne contribue plus
24

Je voudrais ajouter à d'autres réponses, met SynchronizationContext.Postsimplement en file d'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 se poursuit 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 de code (saisie d'une méthode de classe sur le même thread d'exécution avant le retour de l'appel précédent à la même méthode).

Si vous connaissez le modèle de programmation Win32, une analogie très proche serait PostMessage etSendMessage API, 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 dépend du SynchronizationContext .

noseratio
la source
16

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 cela peut être 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 de pool de threads utilisé dans l'extrait de code, 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 déléguée s'exécute sur ce thread.

Attention, cet extrait de code est un peu dangereux, il ne fonctionnera correctement que lorsque vous l'appelez depuis le fil de l'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. Une manière 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é depuis n'importe quel thread. L'avantage d'utiliser 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 TextBox vous avez ici afin que vous sachiez 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 apprendre sur le threading, donc utiliser cet exemple défectueux est correct. Dans la vraie vie, dans les quelques cas où vous pourriez envisager d'utiliser SynchronizationContext.Current, vous laisseriez toujours le soin aux mots-clés async / await de C # ou à TaskScheduler.FromCurrentSynchronizationContext () de le faire pour vous. Mais notez qu'ils se comportent toujours mal comme l'extrait de code lorsque vous les utilisez sur le mauvais thread, pour exactement la même raison. Une question très courante ici, le niveau supplémentaire d'abstraction est utile mais il est plus difficile de comprendre pourquoi ils ne fonctionnent pas correctement. Espérons que le livre vous indique également quand ne pas l'utiliser :)

Hans Passant
la source
Je suis désolé, pourquoi laisser le thread UI gérer est thread-safe? c'est-à-dire que je pense que le thread de l'interface utilisateur pourrait utiliser myTextBox lorsque Post () a été déclenché, est-ce sûr?
nuageuxFan
4
Votre anglais est difficile à décoder. Votre extrait de code d'origine ne fonctionne correctement que lorsqu'il est appelé à partir du thread d'interface utilisateur. Ce qui est un cas très courant. Ce n'est qu'alors qu'il sera publié dans le fil de discussion de l'interface utilisateur. S'il est appelé à partir d'un thread de travail, la cible de délégué Post () s'exécutera sur un thread de pool de threads. Kaboom. C'est quelque chose que vous voulez essayer par vous-même. Démarrez un fil et laissez le fil appeler ce code. Vous l'avez fait correctement si le code se bloque avec une NullReferenceException.
Hans Passant
5

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

Windows exige que les contrôles GUI ne soient accessibles que 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 d'abord (par l'un de plusieurs moyens, comme celui-ci ou le modèle Invoke), une exception sera levée.

Cela permet d'enregistrer 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 exécute le code GUI.

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

Erik Funkenbusch
la source
4
"Oui, le code que vous avez montré est fondamentalement inutile. Pourquoi créer un thread d'arrière-plan, seulement pour avoir immédiatement besoin de revenir au thread principal de l'interface utilisateur? C'est juste un exemple." - La lecture d'un fichier peut être une tâche longue si le fichier est volumineux, quelque chose qui peut bloquer le thread de l'interface utilisateur et le rendre insensible
Yair Nevet
J'ai une question stupide. Chaque thread a un identifiant, et je suppose que le thread UI a également un ID = 2 par exemple. Puis, quand je suis sur le thread du pool de threads, puis-je faire quelque chose comme ça: var thread = GetThread (2); thread.Execute (() => textbox1.Text = "toto")?
John
@John - Non, je ne pense pas que cela fonctionne parce que le thread est déjà en cours d'exécution. Vous ne pouvez pas exécuter un thread déjà en cours d'exécution. L'exécution ne fonctionne que lorsqu'un thread n'est pas en cours d'exécution (IIRC)
Erik Funkenbusch
3

À la source

Chaque thread est associé à un contexte - cela est également connu sous le nom de contexte "courant" - et ces contextes peuvent être partagés entre les threads. Le ExecutionContext contient des métadonnées pertinentes de l'environnement actuel ou du contexte dans lequel le programme est en cours d'exécution. SynchronizationContext représente une abstraction - il indique l'emplacement où le code de votre application est exécuté.

Un SynchronizationContext vous permet de mettre une tâche en file d'attente 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 façon possible de le faire est de demander à Thread2 son objet SynchronizationContext, de le donner à Thread1, puis Thread1 peut appeler SynchronizationContext.Send pour exécuter le code sur Thread2.

Gros yeux
la source
2
Un contexte de synchronisation n'est pas nécessairement lié à un thread particulier. Il est possible que plusieurs threads gèrent des demandes vers un seul contexte de synchronisation et qu'un seul thread gère des demandes pour plusieurs contextes de synchronisation.
Servy
3

SynchronizationContext nous fournit un moyen 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 œil à 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);
        string text = File.ReadAllText(@"c:\temp\log.txt");
        myTextBox.Text = text;
    }

SynchronizationContext.Current renverra le contexte de synchronisation du thread d'interface utilisateur. Comment le sais-je? 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 pendant environ 1 seconde, puis il affichera le contenu du fichier. Vous pourriez vous attendre à ce que ce ne soit pas le cas, car l'appelant de la méthode UpdateTextBox (qui est Work1) est une méthode passée à un thread, il devrait donc dormir ce thread et non le thread d'interface utilisateur principal, NOPE! Même si la méthode Work1 est passée à un thread, notez qu'elle accepte également un objet 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 œil à ce qui suit:

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

Le dernier exemple et celui-ci exécute la même chose. Les deux ne bloquent pas l'interface utilisateur pendant son exécution.

En conclusion, considérez SynchronizationContext comme un 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, c'est comme mettre à jour l'interface normalement à partir du fil de discussion principal de l'interface utilisateur. Si, pour certaines raisons, vous devez mettre à jour l'interface utilisateur à partir d'un thread différent, assurez-vous que ce thread a le SyncContext du thread d'interface utilisateur principal et appelez simplement la méthode Send ou Post dessus avec la méthode que vous souhaitez exécuter et vous êtes tous ensemble.

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

Marc2001
la source
2

SynchronizationContext est essentiellement un fournisseur d'exécution de délégués de rappel principalement responsable de garantir que les délégués sont exécutés dans un contexte d'exécution donné après une partie particulière du code (incorporé dans un objet de tâche de .Net TPL) d'un programme a terminé son exécution.

Du point de vue technique, SC est une classe C # simple qui est orientée pour prendre en charge et fournir sa fonction spécifiquement pour les objets Task Parallel Library.

Chaque application .Net, à l'exception des applications console, a une implémentation particulière de cette classe basée sur le framework sous-jacent spécifique, à savoir: WPF, WindowsForm, Asp Net, Silverlight, etc.

L'importance de cet objet est liée à la synchronisation entre les résultats retournant d'une exécution asyncrone de code et l'exécution de code dépendant qui attend les résultats de ce travail asynchrone.

Et le mot «contexte» signifie contexte d'exécution, c'est-à-dire le contexte d'exécution actuel où ce code en attente sera exécuté, à savoir la synchronisation entre le code asynchrone et son code d'attente se produit dans un contexte d'exécution spécifique, donc cet objet s'appelle SynchronizationContext: il représente le contexte d'exécution qui s'occupera de la synchronisation du code asynchrone et de l'exécution du code en attente .

Ciro Corvino
la source
1

Cet exemple est tiré des exemples 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());
}
loneshark99
la source