Dlaczego obowiązkiem osoby dzwoniącej jest zapewnienie bezpieczeństwa wątków w programowaniu GUI?

37

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:

  1. 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?
  2. 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.

durron597
źródło
1
Ech, wydajność IMHO. Te ogłoszenia o wydarzeniach nie przychodzą za darmo, a w nietrywialnych aplikacjach chcesz zminimalizować ich liczbę (i upewnić się, że żadne nie są zbyt duże), ale logiczna minimalizacja / kondensacja powinna być logicznie przeprowadzona w prezencie.
Ordous,
1
@Ordous Nadal możesz upewnić się, że posty są minimalne, jednocześnie przekazując przekaz wątku w widoku.
durron597,
2
Jakiś czas temu przeczytałem naprawdę dobrego bloga, który omawia ten problem, co w gruncie rzeczy mówi, że bardzo niebezpieczne jest, aby uczynić zestaw interfejsu użytkownika wątkiem bezpiecznym, ponieważ wprowadza możliwe impasy i zależnie od tego, jak jest zaimplementowany, warunki wyścigu w struktura. 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.
MadProgrammer
1
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. Wspominasz o zmianie z Swing na JavaFX, ale miałbyś ten problem z praktycznie każdym frameworkiem interfejsu użytkownika, 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. Niemożliwe jest dokładne określenie, w jaki sposób możesz zaprojektować części API bez interfejsu użytkownika z perspektywy interfejsu API
MadProgrammer
1
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 potrzeb
MadProgrammer

Odpowiedzi:

22

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.

Karl Bielefeldt
źródło
25

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.

maniak zapadkowy
źródło
1
Co ciekawe, rozwiązuję ten problem, mając strukturę kolejkowania dla tych aktualizacji, które napisałem, i które zarządzają przekazywaniem wątków.
durron597,
2
@ durron597: I nigdy nie masz żadnych aktualizacji w zależności od aktualnego stanu interfejsu, na który może wpływać inny wątek? To może zadziałać.
Deduplicator
Dlaczego potrzebujesz zagnieżdżonych zamków? Dlaczego musisz zablokować całe okno główne podczas pracy nad szczegółami w oknie potomnym? Kolejność blokowania jest możliwym do rozwiązania problemem, ponieważ zapewnia jedną odsłoniętą metodę blokowania wielu zamków we właściwej kolejności (albo z góry na dół, albo na dole, ale nie pozostawiaj wyboru dzwoniącemu)
MSalters
1
@MSalters z rootem Miałem na myśli bieżące okno. Aby uzyskać wszystkie blokady, które musisz zdobyć, musisz przejść hierarchię, która wymaga zablokowania każdego kontenera, gdy go napotkasz, zdobycia rodzica i odblokowania (aby upewnić się, że blokujesz tylko od góry), a następnie masz nadzieję, że nie zmienił się cię po zdobyciu okna głównego i wykonaniu blokady z góry na dół.
maniak zapadkowy
@ratchetfreak: Jeśli dziecko, które próbujesz zablokować, zostanie usunięte przez inny wątek podczas blokowania, jest to trochę niefortunne, ale nie ma znaczenia dla blokowania. Po prostu nie możesz operować na obiekcie, który właśnie usunął inny wątek. Ale dlaczego ten inny wątek usuwa obiekty / okna, których wciąż używa Twój wątek? To nie jest dobre w żadnym scenariuszu, nie tylko w interfejsie użytkownika.
MSalters
17

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: gdy Contains(..)a Add(...)i Update(...)jest w pełni uzasadniona API jednego gwintowanego scenariuszu wielowątkowe scenariusz potrzebuje AddOrUpdate.

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.

  1. Zestaw narzędzi nie może rozwiązać tego problemu, ponieważ blokowanie nie zapewnia prawidłowej kolejności operacji.
  2. Widok może rozwiązać problem, ale tylko jeśli zezwolisz na regułę biznesową, że liczba na górze listy musi być zgodna z liczbą elementów na liście i aktualizować listę tylko poprzez widok. Nie do końca taki, jaki powinien być MVC.
  3. Prezenter może to rozwiązać, ale musi zdawać sobie sprawę, że widok ma specjalne potrzeby w zakresie wątków.
  4. Powiązanie danych z modelem obsługującym wiele wątków to kolejna opcja. Ale to komplikuje model z rzeczami, które powinny dotyczyć interfejsu użytkownika.

Ż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.

Patrick
źródło
Być może między Widokiem a Prezenterem można by wprowadzić warstwę, którą można by również wymienić.
durron597,
2
.NET musi System.Windows.Threading.Dispatcherobsł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.
Patrick
9

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.

MadProgrammer
źródło
2
Nie miałbyś tego problemu z siecią. JavaScript celowo nie obsługuje wątków - środowisko wykonawcze JavaScript jest w rzeczywistości tylko jedną dużą kolejką zdarzeń. (Tak, wiem, że ściśle mówiąc JS ma WebWorkers - są to wątłe wątki i zachowują się bardziej jak aktorzy w innych językach).
James_pic
1
@James_pic To, co faktycznie masz, to ta część przeglądarki, która działa jako synchronizator dla kolejki zdarzeń, co w zasadzie mówimy, osoba dzwoniąca była odpowiedzialna za zapewnienie, że aktualizacje będą się pojawiać w kolejce zdarzeń z zestawów narzędzi
MadProgrammer
Tak, dokładnie. Kluczowa różnica w kontekście sieci polega na tym, że synchronizacja ta zachodzi niezależnie od kodu osoby dzwoniącej, ponieważ środowisko wykonawcze nie zapewnia mechanizmu, który pozwalałby na wykonanie kodu poza kolejką zdarzeń. Dzwoniący nie musi więc brać za to odpowiedzialności. Uważam, że jest to również duża część motywacji stojącej za rozwojem NodeJS - jeśli środowisko wykonawcze jest pętlą zdarzeń, wówczas domyślnie cały kod jest pętlą zdarzeń.
James_pic
1
To powiedziawszy, istnieją frameworki interfejsu użytkownika przeglądarki, które mają własną pętlę zdarzeń w pętli zdarzeń (patrzę na ciebie Angular). Ponieważ środowiska te nie są już chronione przez środowisko wykonawcze, wywołujący muszą również zapewnić wykonanie kodu w pętli zdarzeń, podobnie jak w przypadku kodu wielowątkowego w innych środowiskach.
James_pic
Czy pojawiłby się jakiś szczególny problem z automatycznym zapewnianiem bezpieczeństwa wątków przez elementy sterujące „tylko do wyświetlania”? Jeśli ktoś ma edytowalną kontrolkę tekstową, która jest zapisywana przez jeden wątek, a czytana przez inny, nie ma dobrego sposobu, aby uczynić zapis widocznym dla wątku bez synchronizacji co najmniej jednej z tych akcji z rzeczywistym stanem interfejsu użytkownika kontroli, ale czy takie problemy będą miały znaczenie dla elementów sterujących tylko do wyświetlania? Myślę te mogą łatwo dostarczyć wszystkie niezbędne blokady lub blokady dla operacji wykonywanych na nich, aby rozmówca mógł ignorować terminy kiedy ...
SuperCat