Cosa fa SynchronizationContext?


135

Nel libro Programmazione C #, contiene alcuni esempi di codice su SynchronizationContext:

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

Sono un principiante nei thread, quindi per favore rispondi in dettaglio. Innanzitutto, non so cosa significhi contesto, cosa salva il programma in originalContext? E quando Postviene attivato il metodo, cosa farà il thread dell'interfaccia utente?
Se chiedo alcune cose stupide, per favore correggimi, grazie!

EDIT: Ad esempio, cosa succede se scrivo solo myTextBox.Text = text;nel metodo, qual è la differenza?


1
Il fine manuale ha questo da dire Lo scopo del modello di sincronizzazione implementato da questa classe è quello di consentire alle operazioni interne asincrone / di sincronizzazione del Common Language Runtime di comportarsi correttamente con diversi modelli di sincronizzazione. Questo modello semplifica anche alcuni dei requisiti che le applicazioni gestite hanno dovuto seguire per funzionare correttamente in diversi ambienti di sincronizzazione.
ta.speot.is

L'asincrono
dell'IMHO

7
@RoyiNamir: Sì, ma indovina un po ': async/ awaitsi basa SynchronizationContextsotto.
stakx - non contribuisce più il

Risposte:


170

Cosa fa SynchronizationContext?

In poche parole, SynchronizationContextrappresenta una posizione "dove" potrebbe essere eseguito il codice. I delegati passati al suo metodoSend o verranno quindi invocati in quella posizione. ( è la versione non bloccante / asincrona di .)PostPostSend

A ogni thread può essere SynchronizationContextassociata un'istanza. Il thread in esecuzione può essere associato a un contesto di sincronizzazione chiamando il metodo staticoSynchronizationContext.SetSynchronizationContext e il contesto corrente del thread in esecuzione può essere interrogato tramite la SynchronizationContext.Currentproprietà .

Nonostante ciò che ho appena scritto (ogni thread ha un contesto di sincronizzazione associato), a SynchronizationContextnon rappresenta necessariamente un thread specifico ; può anche inoltrare l'invocazione dei delegati trasferiti ad esso su uno dei numerosi thread (ad esempio a un ThreadPoolthread di lavoro) o (almeno in teoria) a un core CPU specifico o persino a un altro host di rete . Il luogo di esecuzione dei delegati dipende dal tipo diSynchronizationContext utilizzo.

Windows Form installerà a WindowsFormsSynchronizationContextsul thread in cui viene creato il primo modulo. (Questo thread viene comunemente chiamato "thread dell'interfaccia utente"). Questo tipo di contesto di sincronizzazione richiama i delegati che gli sono passati esattamente su quel thread. Ciò è molto utile poiché Windows Form, come molti altri framework dell'interfaccia utente, consente solo la manipolazione dei controlli sullo stesso thread su cui sono stati creati.

Cosa succede se scrivo solo myTextBox.Text = text;il metodo, qual è la differenza?

Il codice a cui sei passato ThreadPool.QueueUserWorkItemverrà eseguito su un thread di lavoro del pool di thread. Cioè, non verrà eseguito sul thread su cui è myTextBoxstato creato, quindi Windows Forms prima o poi (specialmente nelle build di rilascio) genererà un'eccezione, indicando che non è possibile accedere myTextBoxda un altro thread.

Questo è il motivo per cui devi in ​​qualche modo "tornare indietro" dal thread di lavoro al "thread dell'interfaccia utente" (dove è myTextBoxstato creato) prima di quella specifica assegnazione. Questo viene fatto come segue:

  1. Mentre sei ancora sul thread dell'interfaccia utente, acquisisci Windows Form ' SynchronizationContextlì e memorizza un riferimento ad esso in una variabile ( originalContext) per un uso successivo. È necessario eseguire una query SynchronizationContext.Currenta questo punto; se lo hai interrogato all'interno del codice passato ThreadPool.QueueUserWorkItem, potresti ottenere qualunque contesto di sincronizzazione sia associato al thread di lavoro del pool di thread. Dopo aver memorizzato un riferimento al contesto di Windows Form, è possibile utilizzarlo ovunque e in qualsiasi momento per "inviare" il codice al thread dell'interfaccia utente.

  2. Ogni volta che è necessario manipolare un elemento dell'interfaccia utente (ma non si trova o potrebbe non esserlo più sul thread dell'interfaccia utente), accedere al contesto di sincronizzazione di Windows Form tramite originalContexte distribuire il codice che manipolerà l'interfaccia utente su Sendo Post.


Osservazioni e suggerimenti finali:

  • Ciò che i contesti di sincronizzazione non fanno per te è dirti quale codice deve essere eseguito in una posizione / contesto specifico e quale codice può essere semplicemente eseguito normalmente, senza passarlo a unSynchronizationContext . Per decidere ciò, è necessario conoscere le regole e i requisiti del framework che si sta programmando - Windows Form in questo caso.

    Quindi ricorda questa semplice regola per Windows Form: NON accedere ai controlli o ai moduli da un thread diverso da quello che li ha creati. Se è necessario, utilizzare il SynchronizationContextmeccanismo come descritto sopra o Control.BeginInvoke(che è un modo specifico di Windows Form di fare esattamente la stessa cosa).

  • Se si programma contro .NET 4.5 o versione successiva, è possibile rendere la vita molto più facile da convertire il codice che esplicitamente usi SynchronizationContext, ThreadPool.QueueUserWorkItem, control.BeginInvoke, ecc al nuovo async/ awaitparole chiave e la Task Parallel Library (TPL) , vale a dire l'API circostante le classi Taske Task<TResult>. Questi, ad un livello molto elevato, si occuperanno di catturare il contesto di sincronizzazione del thread dell'interfaccia utente, avviare un'operazione asincrona, quindi tornare al thread dell'interfaccia utente in modo da poter elaborare il risultato dell'operazione.


Dici che Windows Form, come molti altri framework dell'interfaccia utente, consente solo la manipolazione dei controlli sullo stesso thread ma a tutte le finestre di Windows deve accedere lo stesso thread che lo ha creato.
user34660

4
@ user34660: No, non è corretto. È possibile disporre di più thread che creano controlli di Windows Form. Ma ogni controllo è associato a un thread che lo ha creato e deve essere accessibile solo da quel thread. I controlli da diversi thread dell'interfaccia utente sono anche molto limitati nel modo in cui interagiscono tra loro: uno non può essere il genitore / figlio dell'altro, l'associazione di dati tra loro non è possibile, ecc. Infine, ogni thread che crea controlli ha bisogno del proprio messaggio loop (che viene avviato da Application.RunIIRC). Questo è un argomento piuttosto avanzato e non fatto casualmente.
stakx - non contribuisce più il

Il mio primo commento è dovuto al fatto che hai detto "come molti altri framework dell'interfaccia utente", il che implica che alcune finestre consentono la "manipolazione dei controlli" da un thread diverso, ma nessuna Windows lo fa. Non è possibile "avere diversi thread che creano i controlli di Windows Form" per la stessa finestra e "deve essere accessibile dallo stesso thread" e "deve essere accessibile solo da quel thread" stanno dicendo la stessa cosa. Dubito che sia possibile creare "Controlli da diversi thread dell'interfaccia utente" per la stessa finestra. Tutto ciò non è avanzato per coloro che hanno esperienza con la programmazione Windows prima di .Net.
user34660

3
Tutti questi discorsi su "Windows" e "Windows Windows" mi fanno venire le vertigini. Ho già parlato di una di queste "finestre"? Non penso proprio ...
stakx - non contribuendo più il

1
@ibubi: non sono sicuro di aver capito la tua domanda. Il contesto di sincronizzazione di qualsiasi thread non è né set ( null) né un'istanza di SynchronizationContext(o una sua sottoclasse). Il punto di quella citazione non era quello che ottieni, ma quello che non otterrai: il contesto di sincronizzazione del thread dell'interfaccia utente.
stakx - non contribuisce più il

24

Vorrei aggiungere altre risposte, SynchronizationContext.Postaccoda solo un callback per l'esecuzione successiva sul thread di destinazione (normalmente durante il ciclo successivo del ciclo di messaggi del thread di destinazione), quindi l'esecuzione continua sul thread di chiamata. D'altra parte, SynchronizationContext.Sendtenta di eseguire immediatamente il callback sul thread di destinazione, il che blocca il thread chiamante e può causare deadlock. In entrambi i casi, esiste la possibilità di rientrare nel codice (inserendo un metodo di classe sullo stesso thread di esecuzione prima che la precedente chiamata allo stesso metodo sia ritornata).

Se hai familiarità con il modello di programmazione Win32, un'analogia molto stretta sarebbe PostMessagee SendMessageAPI, che puoi chiamare per inviare un messaggio da un thread diverso da quello della finestra di destinazione.

Ecco una buona spiegazione di cosa siano i contesti di sincronizzazione: È tutto sul SynchronizationContext .


16

Memorizza il provider di sincronizzazione, una classe derivata da SynchronizationContext. In questo caso, sarà probabilmente un'istanza di WindowsFormsSynchronizationContext. Quella classe utilizza i metodi Control.Invoke () e Control.BeginInvoke () per implementare i metodi Send () e Post (). Oppure può essere DispatcherSynchronizationContext, utilizza Dispatcher.Invoke () e BeginInvoke (). In un'app Winforms o WPF, quel provider viene installato automaticamente non appena si crea una finestra.

Quando esegui il codice su un altro thread, come il thread del pool di thread utilizzato nello snippet, devi fare attenzione a non utilizzare direttamente oggetti che non sono sicuri. Come qualsiasi oggetto dell'interfaccia utente, è necessario aggiornare la proprietà TextBox.Text dal thread che ha creato TextBox. Il metodo Post () assicura che la destinazione del delegato venga eseguita su quel thread.

Ricorda che questo frammento è un po 'pericoloso, funzionerà correttamente solo quando lo chiami dal thread dell'interfaccia utente. SynchronizationContext.Current ha valori diversi in thread diversi. Solo il thread dell'interfaccia utente ha un valore utilizzabile. Ed è la ragione per cui il codice ha dovuto copiarlo. Un modo più leggibile e più sicuro per farlo, in un'app Winforms:

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

Che ha il vantaggio che funziona quando viene chiamato da qualsiasi thread. Il vantaggio dell'utilizzo di SynchronizationContext.Current è che funziona ancora se il codice viene utilizzato in Winforms o WPF, è importante in una libreria. Questo è certamente non è un buon esempio di tale codice, si sa sempre che tipo di TextBox che avete qui in modo da sapere sempre se utilizzare o Control.BeginInvoke Dispatcher.BeginInvoke. In realtà l'utilizzo di SynchronizationContext.Current non è così comune.

Il libro sta cercando di insegnarti come usare il threading, quindi usare questo esempio errato è ok. Nella vita reale, nei pochi casi in cui potresti prendere in considerazione l'uso di SynchronizationContext.Current, lo lasceresti comunque alle parole chiave asincrone / in attesa di C # o TaskScheduler.FromCurrentSynchronizationContext () per farlo per te. Tuttavia, tieni presente che si comportano in modo errato nel modo in cui lo snippet fa quando li usi sul thread sbagliato, per lo stesso identico motivo. Una domanda molto comune qui intorno, il livello extra di astrazione è utile ma rende più difficile capire perché non funzionano correttamente. Spero che il libro ti dica anche quando non usarlo :)


Mi dispiace, perché lasciare che l'handle thread dell'interfaccia utente sia thread-safe? cioè penso che il thread dell'interfaccia utente potrebbe usare myTextBox quando Post () è stato attivato, è sicuro?
nuvoloso

4
È difficile decodificare il tuo inglese. Lo snippet originale funziona correttamente solo quando viene chiamato dal thread dell'interfaccia utente. Questo è un caso molto comune. Solo allora verrà postato nuovamente nel thread dell'interfaccia utente. Se viene chiamato da un thread di lavoro, la destinazione del delegato Post () verrà eseguita su un thread di thread pool. Kaboom. Questo è qualcosa che vuoi provare da solo. Avvia un thread e lascia che il thread chiami questo codice. Hai fatto bene se il codice si arresta in modo anomalo con una NullReferenceException.
Hans Passant,

5

Lo scopo del contesto di sincronizzazione qui è quello di assicurarsi che myTextbox.Text = text;venga chiamato sul thread dell'interfaccia utente principale.

Windows richiede che i controlli della GUI siano accessibili solo dal thread con cui sono stati creati. Se si tenta di assegnare il testo in un thread in background senza prima sincronizzarsi (tramite uno dei vari mezzi, come questo o il modello Invoke), verrà generata un'eccezione.

Quello che fa è salvare il contesto di sincronizzazione prima di creare il thread in background, quindi il thread in background utilizza il metodo context.Post esegue il codice GUI.

Sì, il codice che hai mostrato è sostanzialmente inutile. Perché creare un thread in background, solo per tornare immediatamente al thread dell'interfaccia utente principale? È solo un esempio.


4
"Sì, il codice che hai mostrato è sostanzialmente inutile. Perché creare un thread in background, solo per tornare immediatamente al thread dell'interfaccia utente principale? È solo un esempio." - La lettura da un file potrebbe essere un compito lungo se il file è grande, qualcosa che potrebbe bloccare il thread dell'interfaccia utente e renderlo non rispondente
Yair Nevet

Ho una domanda stupida. Ogni thread ha un ID e suppongo che anche il thread dell'interfaccia utente abbia un ID = 2. Quindi, quando sono sul thread del pool di thread, posso fare qualcosa del genere: var thread = GetThread (2); thread.Execute (() => textbox1.Text = "pippo")?
Giovanni,

@John - No, non penso che funzioni perché il thread è già in esecuzione. Non è possibile eseguire un thread già in esecuzione. Execute funziona solo quando un thread non è in esecuzione (IIRC)
Erik Funkenbusch,

3

Alla fonte

Ogni thread ha un contesto ad esso associato - questo è anche conosciuto come il contesto "attuale" - e questi contesti possono essere condivisi tra thread. ExecutionContext contiene metadati rilevanti dell'ambiente o del contesto corrente in cui il programma è in esecuzione. SynchronizationContext rappresenta un'astrazione: indica la posizione in cui viene eseguito il codice dell'applicazione.

Un SynchronizationContext consente di mettere in coda un'attività in un altro contesto. Si noti che ogni thread può avere il proprio SynchronizatonContext.

Ad esempio: supponiamo di avere due thread, Thread1 e Thread2. Dì, Thread1 sta facendo un po 'di lavoro, e poi Thread1 desidera eseguire il codice su Thread2. Un modo possibile per farlo è chiedere a Thread2 il suo oggetto SynchronizationContext, assegnarlo a Thread1, quindi Thread1 può chiamare SynchronizationContext e inviare il codice su Thread2.


2
Un contesto di sincronizzazione non è necessariamente legato a un thread specifico. È possibile che più thread gestiscano le richieste in un singolo contesto di sincronizzazione e che un singolo thread gestisca le richieste per più contesti di sincronizzazione.
Servito il

3

SynchronizationContext ci fornisce un modo per aggiornare un'interfaccia utente da un thread diverso (in modo sincrono tramite il metodo Send o in modo asincrono tramite il metodo Post).

Dai un'occhiata al seguente esempio:

    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 restituirà il contesto di sincronizzazione del thread dell'interfaccia utente. Come faccio a saperlo? All'inizio di ogni modulo o app WPF, il contesto verrà impostato sul thread dell'interfaccia utente. Se crei un'app WPF ed esegui il mio esempio, vedrai che quando fai clic sul pulsante, rimane in sospensione per circa 1 secondo, quindi mostrerà il contenuto del file. Potresti aspettarti che non lo faccia perché il chiamante del metodo UpdateTextBox (che è Work1) è un metodo passato a un thread, quindi dovrebbe dormire quel thread non il thread dell'interfaccia utente principale, NOPE! Anche se il metodo Work1 viene passato a un thread, notare che accetta anche un oggetto che è SyncContext. Se lo guardi, vedrai che il metodo UpdateTextBox viene eseguito attraverso il metodo syncContext.Post e non il metodo Work1. Dai un'occhiata a quanto segue:

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

L'ultimo esempio e questo esegue lo stesso. Entrambi non bloccano l'interfaccia utente mentre fa lavori.

In conclusione, pensa a SynchronizationContext come un thread. Non è un thread, definisce un thread (Nota che non tutti i thread hanno un SyncContext). Ogni volta che chiamiamo il metodo Post o Send su di esso per aggiornare un'interfaccia utente, è proprio come aggiornare l'interfaccia utente normalmente dal thread dell'interfaccia utente principale. Se, per alcuni motivi, devi aggiornare l'interfaccia utente da un thread diverso, assicurati che il thread abbia SyncContext del thread dell'interfaccia utente principale e chiama semplicemente il metodo Send o Post su di esso con il metodo che desideri eseguire e sei tutto impostato.

Spero che questo ti aiuti, amico!


2

SynchronizationContext è fondamentalmente un fornitore di esecuzione di delegati di callback principalmente responsabile di assicurare che i delegati vengano eseguiti in un determinato contesto di esecuzione dopo una particolare porzione di codice (incapsulato in un Task Task di .Net TPL) di un programma ha completato la sua esecuzione.

Dal punto di vista tecnico, SC è una semplice classe C # che è orientata a supportare e fornire la sua funzione specificatamente per gli oggetti Task Parallel Library.

Ogni applicazione .Net, ad eccezione delle applicazioni console, ha un'implementazione particolare di questa classe basata sullo specifico framework sottostante, ovvero: WPF, WindowsForm, Asp Net, Silverlight, ecc.

L'importanza di questo oggetto è legata alla sincronizzazione tra i risultati che ritornano dall'esecuzione asincrona del codice e l'esecuzione del codice dipendente che è in attesa di risultati da quel lavoro asincrono.

E la parola "contesto" sta per contesto di esecuzione, ovvero l'attuale contesto di esecuzione in cui verrà eseguito quel codice di attesa, vale a dire la sincronizzazione tra codice asincrono e il suo codice di attesa avviene in un contesto di esecuzione specifico, quindi questo oggetto è chiamato SynchronizationContext: rappresenta il contesto di esecuzione che si occuperà della sincronizzazione del codice asincrono e dell'esecuzione del codice di attesa .


1

Questo esempio è tratto dagli esempi Linqpad di Joseph Albahari, ma aiuta davvero a capire cosa fa il contesto di sincronizzazione.

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());
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.