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 débutant dans les discussions, veuillez donc répondre en détail. Premièrement, je ne sais pas ce que signifie contexte, qu'est-ce que le programme enregistre dans le originalContext
? Et lorsque la méthode Post
est déclenchée, que fera le thread d'interface utilisateur?
Si je demande des bêtises, corrigez-moi, merci!
EDIT: Par exemple, si je viens d'écrire 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 pourrait être exécuté. Les délégués passés à sa méthode Send
ou Post
seront alors appelés à cet emplacement. (Post
est la version non bloquante/asynchrone de Send
.)
Chaque instance peut être associée à une instance SynchronizationContext
. Le thread en cours d'exécution peut être associé à un contexte de synchronisation en appelant le static SynchronizationContext.SetSynchronizationContext
méthode , et le contexte actuel du thread en cours peut être interrogé via le SynchronizationContext.Current
propriété .
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 transmettre l’invocation des délégués qui lui sont transmis à n’importe quel thread (par exemple, à un ThreadPool
= thread de travail), ou (du moins en théorie) vers un noyau de processeur spécifique , ou même vers un autre réseau Hôte . Le lieu d'exécution de vos délégués 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 délégués qui lui sont transmis sur ce thread. Ceci est très utile car Windows Forms, comme de nombreux autres frameworks d'interface utilisateur, permet uniquement la manipulation de contrôles sur le même thread sur lequel ils ont été créés.
Et si je viens d'écrire
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 de pool de threads. En d’autres termes, il ne s’exécutera pas sur le thread sur lequel votre myTextBox
a été créé. Windows Forms lève tôt ou tard (en particulier dans les versions Release) une exception vous indiquant que vous ne pouvez pas accéder à myTextBox
de l'autre fil.
C'est pourquoi vous devez en quelque sorte "revenir en arrière" du thread de travail sur le "thread d'interface utilisateur" (où myTextBox
a été créé) avant cette affectation particulière. Cela se fait comme suit:
Pendant que vous êtes toujours sur le thread d'interface utilisateur, capturez le SynchronizationContext
de Windows Forms et stockez-y une référence dans une variable (originalContext
) pour une utilisation ultérieure. Vous devez interroger SynchronizationContext.Current
À ce point; 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 Windows Forms, vous pouvez l'utiliser n'importe où et à tout moment pour "envoyer" du code au thread d'interface utilisateur.
À chaque fois que vous devez manipuler un élément d'interface utilisateur (mais que vous ne l'êtes plus ou pas 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. soit Send
ou Post
.
Ce que les contextes de synchronisation ne feront pas vous indique quel code doit être exécuté dans un emplacement/contexte spécifique, et quel code peut simplement être exécuté normalement, sans passer à un SynchronizationContext
. Pour décider de cela, vous devez connaître les règles et les exigences du framework avec lequel vous programmez, Windows Forms dans ce cas.
Alors rappelez-vous cette règle simple pour Windows Forms: N'ACCEDEZ PAS aux contrôles ni aux formulaires à partir d'un thread autre que celui qui les a créés. Si vous devez le faire, utilisez le mécanisme 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 version ultérieure, vous pouvez vous simplifier la vie en convertissant votre code qui utilise explicitement SynchronizationContext
, ThreadPool.QueueUserWorkItem
, control.BeginInvoke
, etc. vers le nouveau async
/await
mots-clés et le bibliothèque parallèle de tâches (TPL) , c'est-à-dire l'API entourant les Task
et Task<TResult>
classes. Celles-ci se chargeront, dans une très grande mesure, 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.
J'aimerais ajouter d'autres réponses, SynchronizationContext.Post
met simplement en file d'attente un rappel pour une exécution ultérieure sur le thread cible (normalement lors du cycle suivant de la boucle de messages du thread cible), puis l'exécution se poursuit sur le thread appelant. D'autre part, SynchronizationContext.Send
tente 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 le retour de l'appel précédent à la même méthode).
Si vous êtes familier avec le modèle de programmation Win32, une analogie très proche serait celle des 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: Tout concerne 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 bien ce 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 d'objets qui ne sont pas thread-safe. Comme tout objet d'interface utilisateur, vous devez mettre à jour la propriété TextBox.Text à partir du thread qui a créé la TextBox. La méthode Post () garantit que la cible de 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 fil de l'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 {
string text = File.ReadAllText(@"c:\temp\log.txt");
myTextBox.BeginInvoke(new Action(() => {
myTextBox.Text = text;
}));
});
Ce qui a l’avantage que cela fonctionne lorsqu’il est appelé depuis n’importe quel fil . L’utilisation de SynchronizationContext.Current présente l’avantage de pouvoir toujours fonctionner, que le code soit utilisé dans Winforms ou WPF, cela importe dans une bibliothèque. Ceci 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, utiliser SynchronizationContext.Current n’est pas si courant.
Le livre essaie de vous apprendre à utiliser le threading, vous pouvez donc utiliser cet exemple imparfait. Dans la vie réelle, dans les rares cas où vous pourriez envisager d'utiliser SynchronizationContext.Current, vous le laisseriez toujours aux mots clés async/wait de C # ou TaskScheduler. .FromCurrentSynchronizationContext () de le faire pour vous. Mais notez qu'ils se comportent toujours mal comme le snippet lorsque vous les utilisez sur le mauvais thread, pour la même raison. Une question très courante ici, le niveau supplémentaire d'abstraction 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 contexte de synchronisation a pour but de s’assurer que myTextbox.Text = text;
est appelé sur le fil principal de l'interface utilisateur.
Windows requiert 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'affecter le texte dans un fil d'arrière-plan sans la synchroniser au préalable (par un moyen quelconque, tel que celui-ci ou le modèle Invoke), une exception sera levée.
Cela permet de sauvegarder le contexte de synchronisation avant de créer le thread d'arrière-plan, qui utilise ensuite la méthode context.Post pour exécuter le code d'interface graphique.
Oui, le code que vous avez montré est fondamentalement inutile. Pourquoi créer un fil d’arrière-plan, alors que vous devez immédiatement revenir au fil principal de l’interface utilisateur? C'est juste un exemple.
SynchronizationContext nous fournit un moyen de mettre à jour une interface utilisateur à partir d'un autre thread (de manière synchrone via la méthode Send ou de manière asynchrone via la méthode Post).
Regardez 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 retournera le contexte de synchronisation du thread UI. Comment je sais ça? Au début de chaque formulaire ou application WPF, le contexte sera défini sur le fil de l'interface utilisateur. Si vous créez une application WPF et exécutez mon exemple, vous verrez que lorsque vous cliquez sur le bouton, il reste en veille environ 1 seconde, puis le contenu du fichier s'affiche. Vous pouvez vous attendre à ce que ce ne soit pas le cas, car l'appelant de la méthode UpdateTextBox (Work1) est une méthode transmise à un thread. Par conséquent, il devrait mettre ce thread en veille et non le thread principal de l'interface utilisateur, NOPE! Même si la méthode Work1 est transmise à un thread, notez qu'elle accepte également un objet qui est le SyncContext. Si vous l'examinez, 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);
string text = File.ReadAllText(@"c:\temp\log.txt");
myTextBox.Text = text;
}
Le dernier exemple et celui-ci exécute le même. Les deux ne bloquent pas l'interface utilisateur pendant qu'il le fait des travaux.
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 principal de l'interface utilisateur. Si, pour certaines raisons, vous devez mettre à jour l'interface utilisateur à partir d'un autre thread, assurez-vous que ce thread possède le SyncContext du thread d'interface utilisateur principal et appelez simplement la méthode Send ou Post avec celle-ci avec la méthode que vous souhaitez exécuter. ensemble.
J'espère que cela vous aide, mec!
Chaque fil est associé à un contexte (également appelé "contexte" actuel) et ces contextes peuvent être partagés entre les fils. 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 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 travaille, 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, qui pourra ensuite appeler SynchronizationContext.Send pour exécuter le code sur Thread2.
Cet exemple est tiré de Linqpad et de Joseph Albahari, mais il aide vraiment à comprendre 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 est fondamentalement un fournisseur d’exécution de délégués de rappel, principalement chargé de s’assurer que les délégués sont exécutés dans un contexte d’exécution donné après une partie de code donnée (incapsulée dans une tâche). obj de .Net TPL) d’un programme a terminé son exécution.
D'un point de vue technique, SC est une simple classe C # orientée de manière à 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, ecc ..
L'importance de cet objet est liée à la synchronisation entre les résultats renvoyés d'une exécution de code asynchrone et l'exécution de code dépendant attendant des résultats de ce travail asynchrone.
Et le mot "contexte" signifie "contexte d'exécution", c'est-à-dire le contexte d'exécution actuel dans lequel le code en attente sera exécuté, à savoir la synchronisation entre le code asynchrone et son code en attente se produisant dans un contexte d'exécution spécifique. Cet objet s'appelle donc SynchronizationContext: il représente le contexte d'exécution qui s'occupera de la synchronisation du code async et de l'exécution du code en attente .