Was macht SynchronizationContext?

134

In dem Buch Programming C # enthält es einen Beispielcode über SynchronizationContext:

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

Ich bin ein Anfänger in Threads, bitte antworten Sie im Detail. Erstens weiß ich nicht, was Kontext bedeutet, was speichert das Programm in der originalContext? Und Postwas macht der UI-Thread, wenn die Methode ausgelöst wird?
Wenn ich ein paar dumme Dinge frage, korrigiere mich bitte, danke!

EDIT: Was ist zum Beispiel, wenn ich nur myTextBox.Text = text;in die Methode schreibe , was ist der Unterschied?

cloudyFan
quelle
1
Das feine Handbuch hat folgendes zu sagen: Der Zweck des von dieser Klasse implementierten Synchronisationsmodells besteht darin, den internen Asynchron- / Synchronisationsoperationen der Common Language Runtime zu ermöglichen, sich mit verschiedenen Synchronisationsmodellen richtig zu verhalten. Dieses Modell vereinfacht auch einige der Anforderungen, die verwaltete Anwendungen erfüllen mussten, um in verschiedenen Synchronisationsumgebungen ordnungsgemäß zu funktionieren.
ta.speot.is
IMHO async warten warten schon das
Royi Namir
7
@RoyiNamir: Ja, aber wissen Sie was: async/ awaitauf verlässt sich SynchronizationContextdarunter.
stakx - nicht mehr beitragen

Antworten:

169

Was macht SynchronizationContext?

Einfach ausgedrückt, SynchronizationContextstellt einen Ort dar, an dem Code ausgeführt werden könnte. Delegaten, die an die MethodeSend oder übergeben Postwerden, werden dann an dieser Stelle aufgerufen. ( Postist die nicht blockierende / asynchrone Version von Send.)

Jedem Thread kann eine SynchronizationContextInstanz zugeordnet sein. Der laufende Thread kann durch Aufrufen der statischen SynchronizationContext.SetSynchronizationContextMethode einem Synchronisationskontext zugeordnet werden , und der aktuelle Kontext des laufenden Threads kann über die SynchronizationContext.CurrentEigenschaft abgefragt werden .

Trotz allem, was ich gerade geschrieben habe (jeder Thread hat einen zugeordneten Synchronisationskontext), SynchronizationContextrepräsentiert a nicht unbedingt einen bestimmten Thread . Es kann auch den Aufruf der an ihn übergebenen Delegaten an einen von mehreren Threads (z. B. an einen ThreadPoolWorker-Thread) oder (zumindest theoretisch) an einen bestimmten CPU-Kern oder sogar an einen anderen Netzwerkhost weiterleiten . Wo Ihre Delegierten am Ende ausgeführt werden, hängt von der Art der SynchronizationContextVerwendung ab.

Windows Forms installiert ein WindowsFormsSynchronizationContextauf dem Thread, auf dem das erste Formular erstellt wird. (Dieser Thread wird üblicherweise als "UI-Thread" bezeichnet.) Diese Art von Synchronisationskontext ruft die an genau diesen Thread übergebenen Delegaten auf. Dies ist sehr nützlich, da Windows Forms wie viele andere UI-Frameworks nur die Manipulation von Steuerelementen in demselben Thread zulässt, in dem sie erstellt wurden.

Was myTextBox.Text = text;ist der Unterschied, wenn ich nur in die Methode schreibe ?

Der Code, an den Sie übergeben haben, ThreadPool.QueueUserWorkItemwird in einem Thread-Pool-Worker-Thread ausgeführt. Das heißt, es wird nicht auf dem Thread ausgeführt, auf dem Sie myTextBoxerstellt wurden, sodass Windows Forms früher oder später (insbesondere in Release-Builds) eine Ausnahme auslöst, die Sie darauf hinweist, dass Sie möglicherweise nicht myTextBoxüber einen anderen Thread zugreifen können .

Aus diesem Grund müssen Sie myTextBoxvor dieser bestimmten Zuweisung vom Worker-Thread zum "UI-Thread" (wo er erstellt wurde) "zurückschalten" . Dies geschieht wie folgt:

  1. Während Sie sich noch im UI-Thread befinden, erfassen Sie dort Windows Forms SynchronizationContextund speichern Sie einen Verweis darauf in einer Variablen ( originalContext) zur späteren Verwendung. Sie müssen SynchronizationContext.Currentan dieser Stelle abfragen . Wenn Sie es innerhalb des übergebenen Codes abgefragt haben ThreadPool.QueueUserWorkItem, erhalten Sie möglicherweise den Synchronisationskontext, der dem Arbeitsthread des Thread-Pools zugeordnet ist. Sobald Sie einen Verweis auf den Kontext von Windows Forms gespeichert haben, können Sie ihn überall und jederzeit verwenden, um Code an den UI-Thread zu "senden".

  2. Wenn Sie ein UI-Element bearbeiten müssen (sich aber nicht mehr im UI-Thread befinden oder möglicherweise nicht mehr befinden), greifen Sie über auf den Synchronisierungskontext von Windows Forms zu originalContextund geben Sie den Code, der die Benutzeroberfläche manipuliert, entweder an Sendoder weiter Post.


Schlussbemerkungen und Hinweise:

  • Was Synchronisationskontexte für Sie nicht tun, ist Ihnen zu sagen, welcher Code an einem bestimmten Ort / Kontext ausgeführt werden muss und welcher Code nur normal ausgeführt werden kann, ohne ihn an a zu übergeben SynchronizationContext. Um dies zu entscheiden, müssen Sie die Regeln und Anforderungen des Frameworks kennen, gegen das Sie programmieren - in diesem Fall Windows Forms.

    Denken Sie also an diese einfache Regel für Windows Forms: Greifen Sie NICHT von einem anderen Thread als dem, der sie erstellt hat, auf Steuerelemente oder Formulare zu. Wenn Sie dies tun müssen, verwenden Sie den SynchronizationContextoben beschriebenen Mechanismus oder Control.BeginInvoke(eine Windows Forms-spezifische Methode, um genau dasselbe zu tun).

  • Wenn Sie die Programmierung gegen .NET 4.5 oder höher, können Sie Ihr Leben viel einfacher machen , indem Sie den Code umzuwandeln , dass explizit Verwendungen SynchronizationContext, ThreadPool.QueueUserWorkItem, control.BeginInvokeetc. auf den neuen async/ awaitKeywords und die Task - Library (TPL) Parallel , dh die API Umgebung die Taskund Task<TResult>Klassen. Diese kümmern sich zu einem sehr hohen Grad darum, den Synchronisationskontext des UI-Threads zu erfassen, eine asynchrone Operation zu starten und dann wieder zum UI-Thread zurückzukehren, damit Sie das Ergebnis der Operation verarbeiten können.

stakx - nicht mehr beitragen
quelle
Sie sagen, Windows Forms erlaubt, wie viele andere UI-Frameworks, nur die Manipulation von Steuerelementen in demselben Thread, aber auf alle Fenster in Windows muss von demselben Thread zugegriffen werden, der es erstellt hat.
user34660
4
@ user34660: Nein, das ist nicht korrekt. Sie können mehrere Threads haben, die Windows Forms-Steuerelemente erstellen. Jedes Steuerelement ist jedoch dem einen Thread zugeordnet, der es erstellt hat, und darf nur von diesem einen Thread aufgerufen werden. Steuerelemente aus verschiedenen UI-Threads sind auch in ihrer Interaktion sehr eingeschränkt: Einer kann nicht das übergeordnete / untergeordnete Element des anderen sein, eine Datenbindung zwischen ihnen ist nicht möglich usw. Schließlich benötigt jeder Thread, der Steuerelemente erstellt, eine eigene Nachricht Schleife (die von Application.RunIIRC gestartet wird ). Dies ist ein ziemlich fortgeschrittenes Thema und nicht beiläufig.
stakx - nicht mehr beitragen
Mein erster Kommentar ist darauf zurückzuführen, dass Sie "wie viele andere UI-Frameworks" sagen, dass einige Fenster die "Manipulation von Steuerelementen" von einem anderen Thread aus zulassen, aber keine Windows-Fenster. Sie können nicht "mehrere Threads haben, die Windows Forms-Steuerelemente erstellen" für dasselbe Fenster und "muss von demselben Thread aufgerufen werden" und "darf nur von diesem einen Thread aufgerufen werden" dasselbe sagen. Ich bezweifle, dass es möglich ist, "Steuerelemente aus verschiedenen UI-Threads" für dasselbe Fenster zu erstellen. All dies ist für diejenigen von uns, die mit Windows-Programmierung vor .Net vertraut sind, nicht fortgeschritten.
user34660
3
Das ganze Gerede über "Windows" und "Windows Windows" macht mich ziemlich schwindelig. Habe ich eines dieser "Fenster" erwähnt? Ich glaube nicht ...
stakx - nicht mehr am
1
@ibubi: Ich bin nicht sicher, ob ich deine Frage verstehe. Der Synchronisationskontext eines Threads ist entweder nicht set ( null) oder eine Instanz von SynchronizationContext(oder eine Unterklasse davon). Der Punkt dieses Zitats war nicht das, was Sie erhalten, sondern das, was Sie nicht erhalten: der Synchronisationskontext des UI-Threads.
stakx - nicht mehr
24

Ich möchte zu anderen Antworten hinzufügen, SynchronizationContext.Postnur einen Rückruf für die spätere Ausführung im Ziel-Thread in die Warteschlange stellen (normalerweise während des nächsten Zyklus der Nachrichtenschleife des Ziel-Threads) und dann die Ausführung im aufrufenden Thread fortsetzen. Versucht andererseits, SynchronizationContext.Sendden Rückruf auf dem Zielthread sofort auszuführen, wodurch der aufrufende Thread blockiert wird und ein Deadlock auftreten kann. In beiden Fällen besteht die Möglichkeit einer erneuten Codeeingabe (Eingabe einer Klassenmethode im selben Ausführungsthread, bevor der vorherige Aufruf derselben Methode zurückgegeben wurde).

Wenn Sie mit Win32 - Programmiermodell vertraut sind, wäre eine sehr enge Analogie sein PostMessageund SendMessageAPIs, die Sie aufrufen können , eine Nachricht von einem Faden unterscheidet sich von dem Zielfenster ein versenden.

Hier ist eine sehr gute Erklärung für Synchronisationskontexte: Alles dreht sich um den Synchronisationskontext .

noseratio
quelle
16

Es speichert den Synchronisationsanbieter, eine von SynchronizationContext abgeleitete Klasse. In diesem Fall handelt es sich wahrscheinlich um eine Instanz von WindowsFormsSynchronizationContext. Diese Klasse verwendet die Methoden Control.Invoke () und Control.BeginInvoke (), um die Methoden Send () und Post () zu implementieren. Oder es kann DispatcherSynchronizationContext sein, es verwendet Dispatcher.Invoke () und BeginInvoke (). In einer Winforms- oder WPF-App wird dieser Anbieter automatisch installiert, sobald Sie ein Fenster erstellen.

Wenn Sie Code in einem anderen Thread ausführen, wie z. B. dem im Snippet verwendeten Thread-Pool-Thread, müssen Sie darauf achten, dass Sie keine direkt threadsicheren Objekte verwenden. Wie bei jedem Benutzeroberflächenobjekt müssen Sie die Eigenschaft TextBox.Text aus dem Thread aktualisieren, der die TextBox erstellt hat. Die Post () -Methode stellt sicher, dass das Delegatenziel auf diesem Thread ausgeführt wird.

Beachten Sie, dass dieses Snippet etwas gefährlich ist. Es funktioniert nur dann ordnungsgemäß, wenn Sie es über den UI-Thread aufrufen. SynchronizationContext.Current hat unterschiedliche Werte in unterschiedlichen Threads. Nur der UI-Thread hat einen verwendbaren Wert. Und ist der Grund, warum der Code es kopieren musste. Eine besser lesbare und sicherere Möglichkeit, dies in einer Winforms-App zu tun:

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

Was den Vorteil hat, dass es funktioniert, wenn es von einem beliebigen Thread aufgerufen wird. Der Vorteil der Verwendung von SynchronizationContext.Current besteht darin, dass es weiterhin funktioniert, unabhängig davon, ob der Code in Winforms oder WPF verwendet wird. Dies ist in einer Bibliothek von Bedeutung. Dies ist sicherlich kein gutes Beispiel für einen solchen Code. Sie wissen immer, welche Art von TextBox Sie hier haben, sodass Sie immer wissen, ob Sie Control.BeginInvoke oder Dispatcher.BeginInvoke verwenden sollen. Tatsächlich ist die Verwendung von SynchronizationContext.Current nicht so häufig.

Das Buch versucht, Ihnen das Einfädeln beizubringen, daher ist es in Ordnung, dieses fehlerhafte Beispiel zu verwenden. In der Praxis können Sie in den wenigen Fällen, in denen Sie die Verwendung von SynchronizationContext.Current in Betracht ziehen, die Schlüsselwörter async / await von C # oder TaskScheduler.FromCurrentSynchronizationContext () für Sie verwenden. Beachten Sie jedoch, dass sie sich aus genau demselben Grund immer noch schlecht verhalten wie das Snippet, wenn Sie sie im falschen Thread verwenden. Eine sehr häufige Frage ist, dass die zusätzliche Abstraktionsebene nützlich ist, es jedoch schwieriger macht, herauszufinden, warum sie nicht richtig funktionieren. Hoffentlich sagt dir das Buch auch, wann du es nicht benutzen sollst :)

Hans Passant
quelle
Es tut mir leid, warum das UI-Thread-Handle threadsicher ist? dh ich denke, der UI-Thread könnte myTextBox verwenden, wenn Post () ausgelöst wird. Ist das sicher?
CloudyFan
4
Ihr Englisch ist schwer zu entschlüsseln. Ihr ursprüngliches Snippet funktioniert nur dann ordnungsgemäß, wenn es vom UI-Thread aufgerufen wird. Welches ist ein sehr häufiger Fall. Erst dann wird es wieder im UI-Thread veröffentlicht. Wenn es von einem Arbeitsthread aufgerufen wird, wird das Post () - Delegatziel in einem Threadpool-Thread ausgeführt. Kaboom. Dies ist etwas, das Sie selbst ausprobieren möchten. Starten Sie einen Thread und lassen Sie den Thread diesen Code aufrufen. Sie haben es richtig gemacht, wenn der Code mit einer NullReferenceException abstürzt.
Hans Passant
5

Der Zweck des Synchronisationskontexts besteht darin, sicherzustellen, dass er myTextbox.Text = text;im Haupt-UI-Thread aufgerufen wird.

Windows erfordert, dass auf GUI-Steuerelemente nur von dem Thread zugegriffen wird, mit dem sie erstellt wurden. Wenn Sie versuchen, den Text in einem Hintergrundthread zuzuweisen, ohne ihn zuvor zu synchronisieren (auf verschiedene Weise, z. B. durch dieses oder das Invoke-Muster), wird eine Ausnahme ausgelöst.

Dadurch wird der Synchronisationskontext vor dem Erstellen des Hintergrundthreads gespeichert. Anschließend verwendet der Hintergrundthread den Kontext. Die Post-Methode führt den GUI-Code aus.

Ja, der von Ihnen angezeigte Code ist grundsätzlich nutzlos. Warum einen Hintergrund-Thread erstellen, um sofort zum Haupt-UI-Thread zurückkehren zu müssen? Es ist nur ein Beispiel.

Erik Funkenbusch
quelle
4
"Ja, der Code, den Sie angezeigt haben, ist im Grunde genommen nutzlos. Warum einen Hintergrund-Thread erstellen, um sofort zum Haupt-UI-Thread zurückkehren zu müssen? Dies ist nur ein Beispiel." - Das Lesen aus einer Datei kann eine lange Aufgabe sein, wenn die Datei groß ist.
Dies
Ich habe eine dumme Frage. Jeder Thread hat eine ID, und ich nehme an, der UI-Thread hat beispielsweise auch eine ID = 2. Wenn ich dann im Thread-Pool-Thread bin, kann ich so etwas tun: var thread = GetThread (2); thread.Execute (() => textbox1.Text = "foo")?
John
@ John - Nein, ich denke nicht, dass das funktioniert, weil der Thread bereits ausgeführt wird. Sie können einen bereits ausgeführten Thread nicht ausführen. Ausführen funktioniert nur, wenn kein Thread ausgeführt wird (IIRC)
Erik Funkenbusch
3

Zur Quelle

Jedem Thread ist ein Kontext zugeordnet - dies wird auch als "aktueller" Kontext bezeichnet - und diese Kontexte können von mehreren Threads gemeinsam genutzt werden. Der ExecutionContext enthält relevante Metadaten der aktuellen Umgebung oder des Kontexts, in dem das Programm ausgeführt wird. Der SynchronizationContext stellt eine Abstraktion dar - er gibt den Ort an, an dem der Code Ihrer Anwendung ausgeführt wird.

Mit einem SynchronizationContext können Sie eine Aufgabe in einen anderen Kontext einreihen. Beachten Sie, dass jeder Thread seinen eigenen SynchronizatonContext haben kann.

Beispiel: Angenommen, Sie haben zwei Threads, Thread1 und Thread2. Angenommen, Thread1 erledigt einige Arbeiten, und dann möchte Thread1 Code auf Thread2 ausführen. Eine Möglichkeit, dies zu tun, besteht darin, Thread2 nach seinem SynchronizationContext-Objekt zu fragen, es Thread1 zu geben, und Thread1 kann dann SynchronizationContext.Send aufrufen, um den Code auf Thread2 auszuführen.

Große Augen
quelle
2
Ein Synchronisationskontext ist nicht unbedingt an einen bestimmten Thread gebunden. Es ist möglich, dass mehrere Threads Anforderungen an einen einzelnen Synchronisationskontext verarbeiten und dass ein einzelner Thread Anforderungen für mehrere Synchronisationskontexte verarbeitet.
Servy
3

SynchronizationContext bietet uns die Möglichkeit, eine Benutzeroberfläche von einem anderen Thread aus zu aktualisieren (synchron über die Send-Methode oder asynchron über die Post-Methode).

Schauen Sie sich das folgende Beispiel an:

    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 gibt den Synchronisierungskontext des UI-Threads zurück. Woher weiß ich das? Zu Beginn jedes Formulars oder jeder WPF-App wird der Kontext im UI-Thread festgelegt. Wenn Sie eine WPF-App erstellen und mein Beispiel ausführen, werden Sie feststellen, dass beim Klicken auf die Schaltfläche etwa 1 Sekunde lang der Ruhezustand angezeigt wird und dann der Inhalt der Datei angezeigt wird. Sie können davon ausgehen, dass dies nicht der Fall ist, da der Aufrufer der UpdateTextBox-Methode (Work1) eine Methode ist, die an einen Thread übergeben wird. Daher sollte dieser Thread nicht der Haupt-UI-Thread NOPE! Beachten Sie, dass die Work1-Methode, obwohl sie an einen Thread übergeben wird, auch ein Objekt akzeptiert, bei dem es sich um den SyncContext handelt. Wenn Sie es sich ansehen, werden Sie feststellen, dass die UpdateTextBox-Methode über die syncContext.Post-Methode und nicht über die Work1-Methode ausgeführt wird. Schauen Sie sich Folgendes an:

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

Das letzte Beispiel und dieses führen dasselbe aus. Beide blockieren die Benutzeroberfläche nicht, während sie Jobs ausführt.

Stellen Sie sich SynchronizationContext abschließend als Thread vor. Es ist kein Thread, es definiert einen Thread (Beachten Sie, dass nicht jeder Thread einen SyncContext hat). Wann immer wir die Post- oder Send-Methode aufrufen, um eine Benutzeroberfläche zu aktualisieren, ist es so, als würde man die Benutzeroberfläche normalerweise über den Haupt-UI-Thread aktualisieren. Wenn Sie aus bestimmten Gründen die Benutzeroberfläche von einem anderen Thread aus aktualisieren müssen, stellen Sie sicher, dass der Thread den SyncContext des Haupt-UI-Threads enthält, und rufen Sie einfach die Send- oder Post-Methode mit der Methode auf, die Sie ausführen möchten, und Sie sind alle einstellen.

Hoffe das hilft dir, Kumpel!

Marc2001
quelle
2

SynchronizationContext ist im Wesentlichen ein Anbieter für die Ausführung von Rückrufdelegierten , der hauptsächlich dafür verantwortlich ist, dass die Delegaten in einem bestimmten Ausführungskontext ausgeführt werden, nachdem ein bestimmter Codeteil (der in einem Taskobjekt von .Net TPL enthalten ist) eines Programms seine Ausführung abgeschlossen hat.

Aus technischer Sicht ist SC eine einfache C # -Klasse, die darauf ausgerichtet ist, ihre Funktion speziell für Task Parallel Library-Objekte zu unterstützen und bereitzustellen.

Jede .NET-Anwendung mit Ausnahme von Konsolenanwendungen verfügt über eine bestimmte Implementierung dieser Klasse, die auf dem spezifischen zugrunde liegenden Framework basiert, z. B.: WPF, WindowsForm, Asp Net, Silverlight usw.

Die Bedeutung dieses Objekts hängt mit der Synchronisation zwischen den Ergebnissen der asynchronen Ausführung von Code und der Ausführung des abhängigen Codes zusammen, der auf die Ergebnisse dieser asynchronen Arbeit wartet.

Und das Wort "Kontext" steht für Ausführungskontext, dh den aktuellen Ausführungskontext, in dem dieser wartende Code ausgeführt wird, nämlich die Synchronisation zwischen asynchronem Code und seinem wartenden Code in einem bestimmten Ausführungskontext. Daher heißt dieses Objekt SynchronizationContext: Es stellt den Ausführungskontext dar, der die Synchronisierung des asynchronen Codes und die Ausführung des wartenden Codes übernimmt .

Ciro Corvino
quelle
1

Dieses Beispiel stammt aus Linqpad-Beispielen von Joseph Albahari, aber es hilft wirklich zu verstehen, was der Synchronisationskontext bewirkt.

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
quelle