Oto najistotniejsze punkty:
- Nie można wykonywać połączenia kontrolne UI z innego wątku niż ten którym zostały utworzone (przewlec formularza).
- Delegowane wywołania (tj. Haki zdarzeń) są wyzwalane w tym samym wątku, co obiekt, który uruchamia zdarzenie.
Tak więc, jeśli masz oddzielną „silnik” wątek jakiejś pracy i mają niektóre UI oglądania dla zmian stanu, które mogą być odzwierciedlone w interfejsie użytkownika (takie jak pasek postępu lub cokolwiek), masz problem. Pożar silnika to obiekt, który zmienił wydarzenie, które zostało zahaczone przez formę. Ale delegat wywołania zwrotnego, że formularz zarejestrowany w silniku zostanie wywołany w wątku silnika ... nie w wątku formularza. Dlatego nie można aktualizować żadnych formantów z tego wywołania zwrotnego. Doh!
BeginInvoke przychodzi na ratunek. Wystarczy użyć tego prostego modelu kodowania wszystkich metod zwrotnych i można mieć pewność, że wszystko będzie w porządku:
private delegate void EventArgsDelegate(object sender, EventArgs ea);
void SomethingHappened(object sender, EventArgs ea)
{
//
// Make sure this callback is on the correct thread
//
if (this.InvokeRequired)
{
this.Invoke(new EventArgsDelegate(SomethingHappened), new object[] { sender, ea });
return;
}
//
// Do something with the event such as update a control
//
textBox1.Text = "Something happened";
}
To naprawdę bardzo proste.
- Zastosowanie InvokeRequired, aby dowiedzieć się, czy to zwrotna się na odpowiednim wątku.
- Jeśli nie, ponownie wywołaj wywołanie zwrotne dla prawidłowego wątku o tych samych parametrach. Metodę można przywrócić, stosując metody Inhibition (blocking) lub BeginInvoke (bez blokowania).
- Następnym razem, gdy funkcja zostanie wywołana, InvokeRequired zwraca wartość false, ponieważ jesteśmy teraz we właściwym wątku i wszyscy są szczęśliwi.
Jest to bardzo kompaktowy sposób rozwiązania tego problemu i zabezpieczenia formularzy przed wielowątkowymi wywołaniami zdarzeń.
Ogólnie preferuję BeginInvoke do Invoke, ale jest pewne zastrzeżenie: należy unikać stania w kolejce zbyt wielu zdarzeń. Używam zmiennej updateRequired, która jest ustawiona na 1, gdy nastąpi BeginInvoke, i wykonuje tylko BeginInvoke, jeśli było zero (za pomocą Interlocked.Exchange). Obsługa wyświetlania ma pętlę while, która usuwa updateRequired, a jeśli nie było zera, wykonuje aktualizację i wykonuje pętle. W niektórych przypadkach dodawany jest zegar, aby dodatkowo ograniczyć częstotliwość aktualizacji (aby uniknąć sytuacji, w której kod spędzał cały czas aktualizując odczyt postępu zamiast wykonywać prawdziwą pracę), ale to jest bardziej skomplikowane. – supercat
@Supercat ... zdarzenie dławienie jest ważnym tematem dla wielu aplikacji, ale nie jest to coś, co powinno być częścią warstwy interfejsu użytkownika. Oddzielna magistrala proxy zdarzeń powinna zostać utworzona w celu odbierania, kolejkowania, łączenia i ponownego wysyłania zdarzeń w odpowiednich odstępach czasu. Dowolny subskrybent magistrali zdarzeń nie powinien wiedzieć, że występuje dławienie zdarzenia. –
Widzę miejsca, w których może być przydatna osobna "magistrala zdarzeń" do obsługi synchronizacji, ale w wielu przypadkach wydaje się najłatwiejszemu użytkownikowi końcowemu coś takiego, jak klasa wskaźnika postępu, jeśli klasa po prostu ujawniła właściwość MinimumUpdateInterval. – supercat