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 Post
metoda 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?
c#
.net
multithreading
pochmurnoFan
źródło
źródło
async
/await
opiera sięSynchronizationContext
pod spodem.Odpowiedzi:
Mówiąc najprościej,
SynchronizationContext
reprezentuje lokalizację, „w której” kod może zostać wykonany. Delegaci, którzy zostaną przesłani do jego metodySend
lubPost
, zostaną następnie wywołani w tej lokalizacji. (Post
jest nieblokującą / asynchroniczną wersjąSend
.)Z każdym wątkiem może być
SynchronizationContext
skojarzona 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średnictwemSynchronizationContext.Current
właściwości .Pomimo tego, co właśnie napisałem (każdy wątek ma skojarzony kontekst synchronizacji),
SynchronizationContext
niekoniecznie reprezentuje konkretny wątek ; może również przekazywać wywołania delegatów przekazanych do niego do dowolnego z kilku wątków (np. doThreadPool
wą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 typuSynchronizationContext
używanego.Windows Forms zainstaluje
WindowsFormsSynchronizationContext
w 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.Kod, który przekazałeś,
ThreadPool.QueueUserWorkItem
będzie uruchamiany w wątku roboczym puli wątków. Oznacza to, że nie będzie wykonywany w wątku, w którymmyTextBox
został 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ępumyTextBox
z innego wątku.Dlatego musisz w jakiś sposób „przełączyć się z powrotem” z wątku roboczego do „wątku UI” (gdzie
myTextBox
został utworzony) przed tym konkretnym przypisaniem. Odbywa się to w następujący sposób:Gdy nadal jesteś w wątku interfejsu użytkownika, przechwyć tam Windows Forms
SynchronizationContext
i zapisz do niego odwołanie w zmiennej (originalContext
) do późniejszego użycia. WSynchronizationContext.Current
tym momencie musisz zapytać ; jeśli odpytałeś go w kodzie przekazanym doThreadPool.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.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
originalContext
i przekaż kod, który będzie manipulował interfejsem użytkownika, doSend
lubPost
.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
SynchronizationContext
mechanizmu opisanego powyżej lubControl.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 nowegoasync
/await
słów kluczowych i Task Parallel Library (TPL) , czyli API okolicyTask
iTask<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.źródło
Application.Run
IIRC). To dość zaawansowany temat i nie jest to coś przypadkowego.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.Chciałbym dodać do innych odpowiedzi,
SynchronizationContext.Post
po 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 stronySynchronizationContext.Send
pró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
PostMessage
iSendMessage
API, 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 .
źródło
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ć :)
źródło
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.
źródło
Do źródła
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.
źródło
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!
źródło
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 .
źródło
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()); }
źródło