Widziałem w wielu miejscach, że zgodnie z mądrością kanoniczną 1 dzwoniący jest odpowiedzialny za upewnienie się, że jesteś w wątku interfejsu użytkownika podczas aktualizacji składników interfejsu użytkownika (w szczególności w Java Swing, że jesteś w wątku wysyłki zdarzeń ) .
Dlaczego tak jest? Nić Event Dispatch jest troska o widoku w MVC / MVP / MVVM; obsłużyć go w dowolnym miejscu, ale widok tworzy ścisłe powiązanie między implementacją widoku a modelem wątków implementacji tego widoku.
Powiedzmy, że mam aplikację zaprojektowaną przez MVC, która wykorzystuje Swing. Jeśli dzwoniący jest odpowiedzialny za aktualizację komponentów w wątku wysyłania zdarzeń, to jeśli spróbuję zamienić moją implementację Swing View na implementację JavaFX, muszę zmienić cały kod Presenter / Controller, aby zamiast tego używać wątku aplikacji JavaFX .
Przypuszczam, że mam dwa pytania:
- Dlaczego to na dzwoniącym spoczywa obowiązek zapewnienia bezpieczeństwa wątku komponentu interfejsu użytkownika? Gdzie jest wada w moim rozumowaniu powyżej?
- Jak mogę zaprojektować aplikację tak, aby miała luźne połączenie tych problemów związanych z bezpieczeństwem nici, a jednocześnie była odpowiednio bezpieczna dla nici?
Pozwólcie, że dodam trochę kodu MCVE Java, aby zilustrować, co rozumiem przez „odpowiedzialny za wywoływanie” (istnieją tutaj inne dobre praktyki, których nie robię, ale celowo staram się być tak minimalny, jak to możliwe):
Osoba dzwoniąca jest odpowiedzialna:
public class Presenter {
private final View;
void updateViewWithNewData(final Data data) {
EventQueue.invokeLater(new Runnable() {
public void run() {
view.setData(data);
}
});
}
}
public class View {
void setData(Data data) {
component.setText(data.getMessage());
}
}
Zobacz odpowiedzialność:
public class Presenter {
private final View;
void updateViewWithNewData(final Data data) {
view.setData(data);
}
}
public class View {
void setData(Data data) {
EventQueue.invokeLater(new Runnable() {
public void run() {
component.setText(data.getMessage());
}
});
}
}
1: Autor tego postu ma najwyższy wynik w tagach Swing on Stack Overflow. Mówi to wszędzie, a także widziałem, że jest to odpowiedzialność osoby dzwoniącej również w innych miejscach.
źródło
Odpowiedzi:
Pod koniec swojego nieudanego eseju marzeń Graham Hamilton (główny architekt Java) wspomina, że jeśli programiści „mają zachować równowagę z modelem kolejek zdarzeń, będą musieli przestrzegać różnych nieoczywistych zasad” oraz mieć widoczny i wyraźny model kolejki zdarzeń „wydaje się pomagać ludziom w bardziej niezawodny sposób podążania za tym modelem, a tym samym w tworzeniu niezawodnych programów GUI”.
Innymi słowy, jeśli spróbujesz nałożyć wielowątkową fasadę na model kolejki zdarzeń, abstrakcja czasami będzie przeciekać w nieoczywisty sposób, który jest wyjątkowo trudny do debugowania. Wygląda na to, że będzie działać na papierze, ale ostatecznie rozpadnie się w produkcji.
Dodanie małych opakowań wokół pojedynczych komponentów prawdopodobnie nie będzie problematyczne, na przykład aktualizacja paska postępu z wątku roboczego. Próba zrobienia czegoś bardziej złożonego, wymagającego wielu blokad, zaczyna być naprawdę trudna do uzasadnienia na temat interakcji warstwy wielowątkowej i warstwy kolejki zdarzeń.
Należy pamiętać, że tego rodzaju problemy są uniwersalne dla wszystkich zestawów narzędzi GUI. Zakładając, że model wysyłania zdarzeń w twoim prezenterie / kontrolerze nie wiąże cię ściśle z tylko jednym modelem współbieżności określonego zestawu narzędzi GUI, łączy cię z nimi wszystkimi . Interfejs kolejkowania zdarzeń nie powinien być trudny do wyodrębnienia.
źródło
Ponieważ zapewnienie bezpieczeństwa wątku lib GUI jest ogromnym bólem głowy i wąskim gardłem.
Przepływ sterowania w GUI często przebiega w 2 kierunkach od kolejki zdarzeń do okna głównego do widgetów GUI oraz od kodu aplikacji do widgetu propagowanego do okna głównego.
Opracowanie strategii blokowania, która nie blokuje okna głównego (spowoduje wiele sporów), jest trudne . Blokowanie od dołu do góry, podczas gdy kolejny wątek blokuje od góry do dołu, jest świetnym sposobem na osiągnięcie natychmiastowego impasu.
Sprawdzanie, czy bieżący wątek jest wątkiem GUI za każdym razem, jest kosztowne i może powodować dezorientację co do tego, co dzieje się z GUI, szczególnie gdy wykonujesz sekwencję zapisu aktualizacji odczytu. To musi mieć blokadę danych, aby uniknąć wyścigów.
źródło
Wątek (w modelu pamięci współdzielonej) jest właściwością, która ma tendencję do przeciwstawiania się abstrakcji. Prostym przykładem jest
Set
-rodzaj: gdyContains(..)
aAdd(...)
iUpdate(...)
jest w pełni uzasadniona API jednego gwintowanego scenariuszu wielowątkowe scenariusz potrzebujeAddOrUpdate
.To samo dotyczy interfejsu użytkownika - jeśli chcesz wyświetlić listę elementów z liczbą elementów na górze listy, musisz zaktualizować oba elementy przy każdej zmianie.
Żadne z tych nie wygląda naprawdę kusząco. Uczynienie prezentera odpowiedzialnym za obsługę wątków jest zalecane nie dlatego, że jest dobre, ale ponieważ działa, a alternatywy są gorsze.
źródło
System.Windows.Threading.Dispatcher
obsługiwać wysyłanie zarówno do wątków interfejsu użytkownika WPF, jak i WinForm. Tego rodzaju warstwa między prezenterem a widokiem zdecydowanie jest przydatna. Ale zapewnia tylko niezależność od zestawu narzędzi, a nie niezależność od wątków.Jakiś czas temu przeczytałem naprawdę dobrego bloga, który omawia ten problem (wspomniany przez Karla Bielefeldta), co w zasadzie mówi, że bardzo niebezpieczne jest, aby uczynić zestaw interfejsu użytkownika wątkiem bezpiecznym, ponieważ wprowadza on potencjalne impasy i zależy od tego, jak to jest zaimplementowano warunki wyścigu do ram.
Istnieje również kwestia wydajności. Nie tak bardzo teraz, ale kiedy Swing został wydany po raz pierwszy, był mocno krytykowany za jego wydajność (był zły), ale to nie była tak naprawdę wina Swinga, to brak wiedzy ludzi na temat korzystania z niego.
SWT egzekwuje koncepcję bezpieczeństwa wątków, wprowadzając wyjątki, jeśli je naruszasz, nie jest to ładne, ale przynajmniej wiesz o tym.
Jeśli spojrzysz na przykład na proces malowania, bardzo ważna jest kolejność malowania elementów. Nie chcesz, aby malowanie jednego elementu powodowało efekt uboczny w jakiejkolwiek innej części ekranu. Wyobraź sobie, że możesz zaktualizować właściwość tekstu etykiety, ale została ona pomalowana przez dwa różne wątki, możesz uzyskać zepsuty wydruk. Więc całe malowanie odbywa się w ramach jednego wątku, zwykle w oparciu o kolejność wymagań / żądań (ale czasami skondensowane w celu zmniejszenia liczby faktycznych fizycznych cykli malowania)
Wspominasz o zmianie z Swing na JavaFX, ale miałbyś ten problem z praktycznie dowolnym frameworkiem interfejsu użytkownika (nie tylko z grubymi klientami, ale także z sieci), Swing wydaje się być tym, który podkreśla problem.
Możesz zaprojektować warstwę pośrednią (kontroler kontrolera?), Której zadaniem jest zapewnienie prawidłowej synchronizacji wywołań interfejsu użytkownika. Z perspektywy interfejsu API interfejsu użytkownika nie można dokładnie wiedzieć, jak zaprojektować części interfejsu API niezwiązane z interfejsem użytkownika, a większość programistów narzekałaby, że ochrona wątków zaimplementowana w interfejsie API interfejsu użytkownika była restrykcyjna lub nie spełniała ich potrzeb. Lepiej pozwolić ci zdecydować, jak chcesz rozwiązać ten problem, w zależności od twoich potrzeb
Jednym z największych problemów, które należy wziąć pod uwagę, jest możliwość uzasadnienia określonej kolejności zdarzeń na podstawie znanych danych wejściowych. Na przykład, jeśli użytkownik zmieni rozmiar okna, model Kolejki zdarzeń gwarantuje, że wystąpi określona kolejność zdarzeń, może się to wydawać proste, ale jeśli kolejka zezwoli na wywołanie zdarzeń przez inne wątki, nie można już zagwarantować kolejności w które zdarzenia mogą się zdarzyć (warunek wyścigu) i nagle musisz zacząć martwić się o różne stany i nie robić jednej rzeczy, dopóki coś innego się nie wydarzy i zaczniesz dzielić flagi państwowe i skończysz spaghetti.
OK, możesz rozwiązać ten problem, mając jakąś kolejkę, która uporządkowała wydarzenia na podstawie czasu ich wydania, ale czy to nie to, co już mamy? Poza tym nadal nie można zagwarantować, że wątek B wygeneruje zdarzenia PO wątku A.
Głównym powodem, dla którego ludzie denerwują się na myśl o swoim kodzie, jest to, że zmuszeni są myśleć o swoim kodzie / projekcie. „Dlaczego to nie może być prostsze?” To nie może być prostsze, ponieważ nie jest to prosty problem.
Pamiętam, kiedy PS3 zostało wydane, a Sony rozmawiało z procesorem Cell i jego zdolnością do wykonywania oddzielnych linii logicznych, dekodowania dźwięku, wideo, ładowania i rozwiązywania danych modelu. Jeden z twórców gier zapytał: „To niesamowite, ale jak synchronizować strumienie?”
Problem, o którym mówił deweloper, w pewnym momencie wszystkie te osobne strumienie musiały być zsynchronizowane w dół do pojedynczego potoku dla wyjścia. Biedny prezenter po prostu wzruszył ramionami, ponieważ nie był to znany im problem. Oczywiście mieli już rozwiązania tego problemu, ale w tamtych czasach było to zabawne.
Współczesne komputery pobierają wiele danych wejściowych z wielu różnych miejsc jednocześnie, wszystkie dane wejściowe muszą zostać przetworzone i dostarczone użytkownikowi z dala, co nie zakłóca prezentacji innych informacji, więc jest to złożony problem, bez jedno proste rozwiązanie.
Teraz, mając możliwość przełączania frameworków, nie jest to łatwe do zaprojektowania, ALE, weźmy MVC na chwilę, MVC może być wielowarstwowa, to znaczy, możesz mieć MVC, która zajmuje się bezpośrednio zarządzaniem frameworkiem UI, mógłby następnie owinąć to, że w MVC wyższej warstwy, która zajmuje się interakcjami z innymi (potencjalnie wielowątkowymi) strukturami, to ta warstwa będzie odpowiedzialna za określenie, w jaki sposób niższa warstwa MVC jest powiadamiana / aktualizowana.
Następnie użyłbyś kodowania do połączenia wzorców projektowych i wzorców fabrycznych lub konstruktorów do budowy tych różnych warstw. Oznacza to, że szkielety wielowątkowe zostają odłączone od warstwy interfejsu użytkownika poprzez użycie warstwy środkowej, jako pomysłu.
źródło