Co robi SynchronizationContext?


140

W książce Programming C # znajduje się przykładowy kod dotyczący SynchronizationContext:

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

Jestem początkującym w wątkach, więc proszę o szczegółowe odpowiedzi. Po pierwsze, nie wiem, co oznacza kontekst, co program zapisuje w pliku originalContext? A kiedy Postmetoda zostanie uruchomiona, co zrobi wątek interfejsu użytkownika?
Jeśli zapytam o głupie rzeczy, popraw mnie, dzięki!

EDYCJA: Na przykład, co jeśli po prostu napiszę myTextBox.Text = text;w metodzie, jaka jest różnica?


1
Dokładny podręcznik ma to do powiedzenia . Celem modelu synchronizacji zaimplementowanego przez tę klasę jest umożliwienie wewnętrznym operacjom asynchronicznym / synchronizacyjnym środowiska uruchomieniowego języka wspólnego, aby zachowywały się prawidłowo z różnymi modelami synchronizacji. Model ten upraszcza również niektóre wymagania, które aplikacje zarządzane musiały spełniać, aby działały poprawnie w różnych środowiskach synchronizacji.
ta.speot.is

Async IMHO już to robi
Royi Namir

7
@RoyiNamir: Tak, ale zgadnij, na czym: async/ awaitopiera się SynchronizationContextpod spodem.
stakx - nie publikuje już

Odpowiedzi:


173

Co robi SynchronizationContext?

Mówiąc najprościej, SynchronizationContextreprezentuje lokalizację, „w której” kod może zostać wykonany. Delegaci, którzy zostaną przesłani do jego metodySend lub Post, zostaną następnie wywołani w tej lokalizacji. ( Postjest nieblokującą / asynchroniczną wersją Send.)

Z każdym wątkiem może być SynchronizationContextskojarzona instancja. Działający wątek można powiązać z kontekstem synchronizacji, wywołując metodę statycznąSynchronizationContext.SetSynchronizationContext , a bieżący kontekst działającego wątku można zapytać za pośrednictwem SynchronizationContext.Currentwłaściwości .

Pomimo tego, co właśnie napisałem (każdy wątek ma skojarzony kontekst synchronizacji), SynchronizationContextniekoniecznie reprezentuje konkretny wątek ; może również przekazywać wywołania delegatów przekazanych do niego do dowolnego z kilku wątków (np. do ThreadPoolwątku roboczego) lub (przynajmniej w teorii) do określonego rdzenia procesora lub nawet do innego hosta sieciowego . To, gdzie delegaci kończą pracę, zależy od typu SynchronizationContextużywanego.

Windows Forms zainstaluje WindowsFormsSynchronizationContextw wątku, w którym jest tworzony pierwszy formularz. (Ten wątek jest powszechnie nazywany „wątkiem interfejsu użytkownika”). Ten typ kontekstu synchronizacji wywołuje delegatów przekazanych do niego dokładnie w tym wątku. Jest to bardzo przydatne, ponieważ Windows Forms, podobnie jak wiele innych struktur interfejsu użytkownika, zezwala tylko na manipulowanie kontrolkami w tym samym wątku, w którym zostały utworzone.

A co jeśli po prostu napiszę myTextBox.Text = text;w metodzie, jaka jest różnica?

Kod, który przekazałeś, ThreadPool.QueueUserWorkItembędzie uruchamiany w wątku roboczym puli wątków. Oznacza to, że nie będzie wykonywany w wątku, w którym myTextBoxzostał utworzony, więc Windows Forms prędzej czy później (szczególnie w kompilacjach wydania) zgłosi wyjątek, informujący, że możesz nie uzyskać dostępu myTextBoxz innego wątku.

Dlatego musisz w jakiś sposób „przełączyć się z powrotem” z wątku roboczego do „wątku UI” (gdzie myTextBoxzostał utworzony) przed tym konkretnym przypisaniem. Odbywa się to w następujący sposób:

  1. Gdy nadal jesteś w wątku interfejsu użytkownika, przechwyć tam Windows Forms SynchronizationContexti zapisz do niego odwołanie w zmiennej ( originalContext) do późniejszego użycia. W SynchronizationContext.Currenttym momencie musisz zapytać ; jeśli odpytałeś go w kodzie przekazanym do ThreadPool.QueueUserWorkItem, możesz uzyskać dowolny kontekst synchronizacji powiązany z wątkiem roboczym puli wątków. Po zapisaniu odniesienia do kontekstu Windows Forms można go używać w dowolnym miejscu i czasie w celu „wysłania” kodu do wątku interfejsu użytkownika.

  2. Za każdym razem, gdy musisz manipulować elementem interfejsu użytkownika (ale nie ma go lub może już nie być w wątku interfejsu użytkownika), uzyskaj dostęp do kontekstu synchronizacji formularzy systemu Windows za pośrednictwem originalContexti przekaż kod, który będzie manipulował interfejsem użytkownika, do Sendlub Post.


Uwagi końcowe i wskazówki:

  • To, czego konteksty synchronizacji nie zrobią, to wskazanie, który kod musi działać w określonej lokalizacji / kontekście, a który kod można po prostu wykonać normalnie, bez przekazywania go do pliku SynchronizationContext. Aby się na to zdecydować, musisz znać reguły i wymagania frameworka, w którym programujesz - w tym przypadku Windows Forms.

    Pamiętaj więc o prostej zasadzie dla Windows Forms: NIE uzyskuj dostępu do formantów lub formularzy z wątku innego niż ten, który je utworzył. Jeśli musisz to zrobić, użyj SynchronizationContextmechanizmu opisanego powyżej lub Control.BeginInvoke(który jest specyficznym dla Windows Forms sposobem robienia dokładnie tego samego).

  • Jeśli jesteś programowanie przeciwko .NET 4.5 lub nowszy, można uczynić Twoje życie łatwiejszym poprzez konwersję kodu jawnie zastosowania SynchronizationContext, ThreadPool.QueueUserWorkItem, control.BeginInvoke, itd. Do nowego async/ awaitsłów kluczowych i Task Parallel Library (TPL) , czyli API okolicy Taski Task<TResult>klas. Te w bardzo wysokim stopniu zajmą się przechwytywaniem kontekstu synchronizacji wątku interfejsu użytkownika, uruchamianiem operacji asynchronicznej, a następnie powrotem do wątku interfejsu użytkownika, aby można było przetworzyć wynik operacji.


Mówisz, że Windows Forms, podobnie jak wiele innych struktur interfejsu użytkownika, zezwala tylko na manipulowanie kontrolkami w tym samym wątku, ale wszystkie okna w systemie Windows muszą być dostępne przez ten sam wątek, który go utworzył.
user34660

4
@ user34660: Nie, to nieprawda. Państwo może mieć kilka wątków, które tworzyć formularze sterujących Windows. Ale każda kontrolka jest skojarzona z jednym wątkiem, który ją utworzył i musi być dostępna tylko przez ten jeden wątek. Kontrolki z różnych wątków interfejsu użytkownika są również bardzo ograniczone pod względem interakcji ze sobą: jedna nie może być nadrzędna / podrzędna drugiej, powiązanie danych między nimi nie jest możliwe itd. Wreszcie, każdy wątek, który tworzy formanty, potrzebuje własnego komunikatu pętla (która zaczyna się przez Application.RunIIRC). To dość zaawansowany temat i nie jest to coś przypadkowego.
stakx - nie publikuje już

Mój pierwszy komentarz jest spowodowany tym, że powiedziałeś „jak wiele innych frameworków interfejsu użytkownika”, sugerując, że niektóre okna umożliwiają „manipulowanie kontrolkami” z innego wątku, ale żadne okna Windows tego nie robią. Nie można „mieć kilku wątków, które tworzą kontrolki Windows Forms” dla tego samego okna i „muszą być dostępne dla tego samego wątku” i „muszą być dostępne tylko przez ten jeden wątek” mówią to samo. Wątpię, czy jest możliwe utworzenie „Kontroli z różnych wątków UI” dla tego samego okna. Wszystko to nie jest zaawansowane dla tych z nas, którzy mieli doświadczenie w programowaniu w systemie Windows przed .Net.
user34660

3
Cała ta rozmowa o „oknach” i „oknach Windows” przyprawia mnie o zawrót głowy. Czy wspomniałem o którymkolwiek z tych „okien”? Nie sądzę ...
stakx - już nie publikuję

1
@ibubi: Nie jestem pewien, czy rozumiem twoje pytanie. Kontekst synchronizacji żadnego wątku nie jest ustawiony ( null) lub jest instancją SynchronizationContext(lub jej podklasą). Celem tego cytatu nie było to, co otrzymujesz, ale to, czego nie otrzymasz: kontekst synchronizacji wątku interfejsu użytkownika.
stakx - nie publikuje już

26

Chciałbym dodać do innych odpowiedzi, SynchronizationContext.Postpo prostu kolejkuje wywołanie zwrotne do późniejszego wykonania w wątku docelowym (zwykle podczas następnego cyklu pętli komunikatów wątku docelowego), a następnie wykonywanie jest kontynuowane w wątku wywołującym. Z drugiej strony SynchronizationContext.Sendpróbuje natychmiast wykonać wywołanie zwrotne w wątku docelowym, co blokuje wątek wywołujący i może spowodować zakleszczenie. W obu przypadkach istnieje możliwość ponownego wejścia kodu (wpisanie metody klasy w tym samym wątku wykonania przed zwróceniem poprzedniego wywołania tej samej metody).

Jeśli znasz model programowania Win32, bardzo bliską analogią byłoby PostMessagei SendMessageAPI, które możesz wywołać w celu wysłania wiadomości z wątku innego niż ten w oknie docelowym.

Oto bardzo dobre wyjaśnienie, jakie są konteksty synchronizacji: To wszystko dotyczy kontekstu synchronizacji .


16

Przechowuje dostawcę synchronizacji, klasę pochodzącą z SynchronizationContext. W tym przypadku prawdopodobnie będzie to wystąpienie WindowsFormsSynchronizationContext. Ta klasa używa metod Control.Invoke () i Control.BeginInvoke () do implementacji metod Send () i Post (). Lub może to być DispatcherSynchronizationContext, używa Dispatcher.Invoke () i BeginInvoke (). W aplikacji Winforms lub WPF ten dostawca jest automatycznie instalowany zaraz po utworzeniu okna.

Kiedy uruchamiasz kod w innym wątku, takim jak wątek puli wątków używany we fragmencie, musisz uważać, aby nie używać bezpośrednio obiektów, które nie są bezpieczne dla wątków. Podobnie jak w przypadku każdego obiektu interfejsu użytkownika, należy zaktualizować właściwość TextBox.Text z wątku, który utworzył TextBox. Metoda Post () zapewnia, że ​​docelowy delegat działa w tym wątku.

Uważaj, ten fragment kodu jest nieco niebezpieczny, będzie działał poprawnie tylko wtedy, gdy wywołasz go z wątku interfejsu użytkownika. SynchronizationContext.Current ma różne wartości w różnych wątkach. Tylko wątek interfejsu użytkownika ma użyteczną wartość. I to jest powód, dla którego kod musiał go skopiować. Czytelniejszy i bezpieczniejszy sposób na zrobienie tego w aplikacji Winforms:

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

Co ma tę zaletę, że działa przy wywołaniu z dowolnego wątku. Zaletą korzystania z SynchronizationContext.Current jest to, że nadal działa niezależnie od tego, czy kod jest używany w Winforms czy WPF, ma to znaczenie w bibliotece. Z pewnością nie jest to dobry przykład takiego kodu, zawsze wiesz, jaki masz tutaj TextBox, więc zawsze wiesz, czy użyć Control.BeginInvoke czy Dispatcher.BeginInvoke. W rzeczywistości używanie SynchronizationContext.Current nie jest tak powszechne.

Książka próbuje cię nauczyć o gwintowaniu, więc użycie tego błędnego przykładu jest w porządku. W prawdziwym życiu, w nielicznych przypadkach, gdy może rozważyć użycie SynchronizationContext.Current, że ty nadal pozostawić go do C # 's async / Oczekujcie słowa kluczowe lub TaskScheduler.FromCurrentSynchronizationContext (), aby zrobić to za Ciebie. Ale pamiętaj, że nadal źle zachowują się tak, jak fragment kodu, gdy używasz ich w niewłaściwym wątku, z dokładnie tego samego powodu. Bardzo częste tutaj pytanie, dodatkowy poziom abstrakcji jest przydatny, ale utrudnia ustalenie, dlaczego nie działają poprawnie. Mam nadzieję, że książka podpowie Ci, kiedy jej nie używać :)


Przepraszam, dlaczego pozwalam, aby uchwyt wątku interfejsu użytkownika był bezpieczny dla wątków? tj. myślę, że wątek interfejsu użytkownika może używać myTextBox po uruchomieniu funkcji Post (), czy to jest bezpieczne?
cloudyFan,

4
Twój angielski jest trudny do rozszyfrowania. Twój oryginalny fragment kodu działa poprawnie tylko wtedy, gdy jest wywoływany z wątku interfejsu użytkownika. Co jest bardzo częstym przypadkiem. Dopiero wtedy powróci do wątku interfejsu użytkownika. Jeśli zostanie wywołany z wątku roboczego, cel delegata Post () będzie działał w wątku puli wątków. Kaboom. To jest coś, czego sam chcesz spróbować. Uruchom wątek i pozwól wątkowi wywołać ten kod. Zrobiłeś to dobrze, jeśli kod ulegnie awarii z NullReferenceException.
Hans Passant

5

Celem kontekstu synchronizacji jest tutaj upewnienie się, że myTextbox.Text = text;zostanie wywołany w głównym wątku interfejsu użytkownika.

Windows wymaga, aby kontrolki GUI były dostępne tylko przez wątek, w którym zostały utworzone. Jeśli spróbujesz przypisać tekst w wątku w tle bez uprzedniej synchronizacji (za pomocą dowolnej z kilku metod, takich jak ten lub wzorzec Invoke), zostanie zgłoszony wyjątek.

To, co robi, to zapisanie kontekstu synchronizacji przed utworzeniem wątku w tle, a następnie wątek w tle używa context. Metoda Post wykonuje kod GUI.

Tak, kod, który pokazałeś, jest w zasadzie bezużyteczny. Po co tworzyć wątek w tle, tylko po to, aby natychmiast wrócić do głównego wątku interfejsu użytkownika? To tylko przykład.


4
„Tak, kod, który pokazałeś, jest w zasadzie bezużyteczny. Po co tworzyć wątek w tle, tylko po to, aby natychmiast wrócić do głównego wątku interfejsu użytkownika? To tylko przykład”. - Czytanie z pliku może być długim zadaniem, jeśli plik jest duży, coś, co może zablokować wątek interfejsu użytkownika i sprawić, że
przestanie

Mam głupie pytanie. Każdy wątek ma identyfikator i przypuszczam, że wątek interfejsu użytkownika ma również na przykład identyfikator = 2. Następnie, gdy jestem w wątku puli wątków, czy mogę zrobić coś takiego: var thread = GetThread (2); thread.Execute (() => textbox1.Text = "foo")?
John

@John - Nie, nie sądzę, żeby to działało, ponieważ wątek już się wykonuje. Nie możesz wykonać już wykonywanego wątku. Wykonaj działa tylko wtedy, gdy wątek nie jest uruchomiony (IIRC)
Erik Funkenbusch

4

Do źródła

Każdy wątek ma powiązany z nim kontekst - nazywany również kontekstem „bieżącym” - i te konteksty mogą być współużytkowane przez wątki. ExecutionContext zawiera odpowiednie metadane bieżącego środowiska lub kontekstu, w którym program jest wykonywany. SynchronizationContext reprezentuje abstrakcję - oznacza lokalizację, w której jest wykonywany kod aplikacji.

SynchronizationContext umożliwia kolejkowanie zadania w innym kontekście. Zauważ, że każdy wątek może mieć swój własny SynchronizatonContext.

Na przykład: załóżmy, że masz dwa wątki, Thread1 i Thread2. Powiedzmy, że Thread1 wykonuje jakąś pracę, a następnie Thread1 chce wykonać kod na Thread2. Jednym z możliwych sposobów jest poproszenie Thread2 o jego obiekt SynchronizationContext, przekazanie go Thread1, a następnie Thread1 może wywołać SynchronizationContext.Send w celu wykonania kodu w Thread2.


2
Kontekst synchronizacji niekoniecznie jest powiązany z określonym wątkiem. Możliwe jest, że wiele wątków obsługuje żądania do jednego kontekstu synchronizacji, a jeden wątek obsługuje żądania dotyczące wielu kontekstów synchronizacji.
Servy

3

SynchronizationContext zapewnia nam sposób aktualizowania interfejsu użytkownika z innego wątku (synchronicznie za pomocą metody Send lub asynchronicznie za pomocą metody Post).

Spójrz na następujący przykład:

    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 zwróci kontekst synchronizacji wątku interfejsu użytkownika. Skąd to wiem? Na początku każdego formularza lub aplikacji WPF kontekst zostanie ustawiony w wątku interfejsu użytkownika. Jeśli utworzysz aplikację WPF i uruchomisz mój przykład, zobaczysz, że po kliknięciu przycisku zasypia przez około 1 sekundę, a następnie wyświetli zawartość pliku. Można się spodziewać, że tak się nie stanie, ponieważ obiekt wywołujący metodę UpdateTextBox (czyli Work1) jest metodą przekazaną do wątku, dlatego powinien spać ten wątek, a nie główny wątek interfejsu użytkownika, NIE! Mimo że metoda Work1 jest przekazywana do wątku, zwróć uwagę, że akceptuje również obiekt, który jest SyncContext. Jeśli na to spojrzysz, zobaczysz, że metoda UpdateTextBox jest wykonywana za pomocą metody syncContext.Post, a nie metody Work1. Spójrz na następujące kwestie:

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

Ostatni przykład i ten wykonują to samo. Oba nie blokują interfejsu użytkownika, gdy wykonuje swoje zadania.

Podsumowując, pomyśl o SynchronizationContext jako o wątku. To nie jest wątek, definiuje wątek (zwróć uwagę, że nie każdy wątek ma SyncContext). Za każdym razem, gdy wywołujemy metodę Post lub Send w celu zaktualizowania interfejsu użytkownika, jest to tak samo, jak normalna aktualizacja interfejsu użytkownika z głównego wątku interfejsu użytkownika. Jeśli z jakichś powodów musisz zaktualizować interfejs użytkownika z innego wątku, upewnij się, że wątek ma SyncContext głównego wątku interfejsu użytkownika i po prostu wywołaj na nim metodę Send lub Post z metodą, którą chcesz wykonać, i jesteś wszystkim zestaw.

Mam nadzieję, że to ci pomoże, kolego!


2

SynchronizationContext w zasadzie jest dostawcą wykonywania delegatów wywołania zwrotnego odpowiedzialnym głównie za zapewnienie, że delegaci są uruchamiani w danym kontekście wykonania po zakończeniu wykonywania określonej części kodu (zawartego w obiekcie Task obj .Net TPL) programu.

Z technicznego punktu widzenia SC jest prostą klasą C #, która jest zorientowana na obsługę i zapewnianie swojej funkcji specjalnie dla obiektów biblioteki zadań równoległych.

Każda aplikacja .Net, z wyjątkiem aplikacji konsolowych, ma określoną implementację tej klasy opartą na określonej strukturze bazowej, tj .: WPF, WindowsForm, Asp Net, Silverlight, ecc ..

Znaczenie tego obiektu jest związane z synchronizacją między wynikami powracającymi z asynchronicznego wykonania kodu a wykonaniem zależnego kodu, który oczekuje na wyniki tej asynchronicznej pracy.

A słowo „kontekst” oznacza kontekst wykonania, czyli bieżący kontekst wykonania, w którym ten oczekujący kod zostanie wykonany, a mianowicie synchronizacja między kodem asynchronicznym a kodem oczekującym odbywa się w określonym kontekście wykonania, dlatego ten obiekt nosi nazwę SynchronizationContext: reprezentuje kontekst wykonania, który będzie dbał o synchronizację kodu asynchronicznego i wykonanie kodu oczekującego .


1

Ten przykład pochodzi z przykładów Linqpada autorstwa Josepha Albahariego, ale naprawdę pomaga w zrozumieniu, co robi kontekst synchronizacji.

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());
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.