Wątki w aplikacji PyQt: Używasz wątków Qt lub wątków Python?

117

Piszę aplikację GUI, która regularnie pobiera dane przez połączenie internetowe. Ponieważ pobieranie trwa chwilę, powoduje to, że interfejs użytkownika nie odpowiada podczas procesu pobierania (nie można go podzielić na mniejsze części). Dlatego chciałbym przekazać połączenie internetowe do osobnego wątku roboczego.

[Tak, wiem, teraz mam dwa problemy .]

W każdym razie aplikacja korzysta z PyQt4, więc chciałbym wiedzieć, jaki jest lepszy wybór: użyć wątków Qt czy użyć threadingmodułu Python ? Jakie są zalety / wady każdego z nich? A może masz zupełnie inną sugestię?

Edycja (re bounty): Chociaż rozwiązaniem w moim konkretnym przypadku prawdopodobnie będzie użycie nieblokującego żądania sieciowego, takiego jak zasugerowali Jeff Ober i Lukáš Lalinský (więc zasadniczo pozostawiając problemy z współbieżnością implementacji sieciowej), nadal chciałbym uzyskać więcej dogłębna odpowiedź na ogólne pytanie:

Jakie są zalety i wady używania wątków PyQt4 (tj. Qt) w porównaniu z natywnymi wątkami Pythona (z threadingmodułu)?


Edycja 2: Dziękuję wszystkim za odpowiedzi. Chociaż nie ma 100% zgodności, wydaje się, że istnieje powszechna zgoda co do tego, że odpowiedzią jest „użyj Qt”, ponieważ zaletą tego jest integracja z resztą biblioteki, która nie powoduje żadnych rzeczywistych wad.

Wszystkim, którzy chcą wybrać jedną z dwóch implementacji wątków, bardzo polecam przeczytanie wszystkich odpowiedzi tutaj udzielonych, w tym wątku listy dyskusyjnej PyQt, do którego opat prowadzi linki.

Było kilka odpowiedzi, które rozważałem w sprawie nagrody; w końcu wybrałem opata jako bardzo istotne odniesienie zewnętrzne; było to jednak mało prawdopodobne.

Dzięki jeszcze raz.

balpha
źródło

Odpowiedzi:

107

Zostało to omówione nie tak dawno temu na liście mailingowej PyQt. Cytowanie Giovanni Bajo za komentarze na ten temat:

To w większości to samo. Główna różnica polega na tym, że QThreads są lepiej zintegrowane z Qt (asynchroniczne sygnały / szczeliny, pętla zdarzeń itp.). Ponadto nie możesz używać Qt z wątku Pythona (nie możesz na przykład wysyłać zdarzenia do głównego wątku przez QApplication.postEvent): potrzebujesz QThread, aby to działało.

Ogólna praktyczna zasada może polegać na używaniu QThreads, jeśli zamierzasz w jakiś sposób wchodzić w interakcję z Qt, a inaczej używać wątków Pythona.

I kilka wcześniejszych komentarzy na ten temat od autora PyQt: „Oba są opakowaniami wokół tych samych implementacji wątków natywnych”. Obie implementacje używają GIL w ten sam sposób.

opat
źródło
2
Dobra odpowiedź, ale myślę, że powinieneś używać przycisku z cytatem
blokowym
2
Zastanawiam się, dlaczego nie możesz publikować wydarzeń w głównym wątku za pośrednictwem QApplication.postEvent () i potrzebujesz do tego QThread? Myślę, że widziałem ludzi, którzy to robili i zadziałało.
Trilarion
1
Dzwoniłem QCoreApplication.postEventz wątku Pythona z szybkością 100 razy na sekundę w aplikacji, która działa na wielu platformach i była testowana przez tysiące godzin. Nigdy nie widziałem z tym żadnych problemów. Myślę, że jest to w porządku, o ile obiekt docelowy znajduje się w MainThread lub QThread. Umieściłem go również w ładnej bibliotece, zobacz qtutils .
three_pineapples
2
Biorąc pod uwagę bardzo pozytywny charakter tego pytania i odpowiedzi, myślę, że warto wskazać niedawną odpowiedź SO autorstwa ekhumoro, która omawia warunki, w których można bezpiecznie używać niektórych metod Qt z wątków Pythona. Jest to zgodne z obserwowanym zachowaniem, które widziałem ja i @Trilarion.
three_pineapples
33

Wątki Pythona będą prostsze i bezpieczniejsze, a ponieważ są przeznaczone dla aplikacji opartych na I / O, są w stanie ominąć GIL. To powiedziawszy, czy rozważałeś nieblokujące wejścia / wyjścia za pomocą skręconych lub nieblokujących gniazd / wybierz?

EDYCJA: więcej na temat wątków

Wątki Pythona

Wątki Pythona są wątkami systemowymi. Jednak Python używa globalnej blokady interpretera (GIL), aby upewnić się, że interpreter zawsze wykonuje w danym momencie tylko blok instrukcji kodu bajtowego o określonej wielkości. Na szczęście Python zwalnia GIL podczas operacji wejścia / wyjścia, dzięki czemu wątki są przydatne do symulowania nieblokujących operacji we / wy.

Ważne ostrzeżenie: może to być mylące, ponieważ liczba instrukcji kodu bajtowego nie odpowiada liczbie wierszy w programie. Nawet pojedyncze przypisanie może nie być atomowe w Pythonie, więc blokada mutex jest konieczna dla każdego bloku kodu, który musi być wykonywany atomowo, nawet z GIL.

Wątki QT

Kiedy Python przekazuje kontrolę skompilowanemu modułowi strony trzeciej, zwalnia GIL. Obowiązkiem modułu jest zapewnienie atomowości tam, gdzie jest to wymagane. Kiedy kontrola jest przekazywana z powrotem, Python użyje GIL. Może to utrudniać korzystanie z bibliotek innych firm w połączeniu z wątkami. Korzystanie z zewnętrznej biblioteki wątków jest jeszcze trudniejsze, ponieważ zwiększa to niepewność, gdzie i kiedy kontrola jest w rękach modułu, a kiedy interpretera.

Wątki QT działają z wydanym GIL. Wątki QT mogą jednocześnie wykonywać kod biblioteki QT (i inny skompilowany kod modułu, który nie pobiera GIL). Jednak kod Pythona wykonywany w kontekście wątku QT nadal uzyskuje GIL i teraz musisz zarządzać dwoma zestawami logiki do blokowania kodu.

W końcu zarówno wątki QT, jak i wątki Pythona są opakowaniami wokół wątków systemowych. Wątki Pythona są marginalnie bezpieczniejsze w użyciu, ponieważ te części, które nie są napisane w Pythonie (niejawnie przy użyciu GIL), używają GIL w każdym przypadku (chociaż powyższe zastrzeżenie nadal ma zastosowanie).

Nieblokujące we / wy

Wątki niezwykle komplikują Twoją aplikację. Zwłaszcza, gdy mamy do czynienia z już złożoną interakcją między interpretera Pythona a skompilowanym kodem modułu. Podczas gdy wielu osobom trudno jest śledzić programowanie oparte na zdarzeniach, nieblokujące operacje we / wy oparte na zdarzeniach są często znacznie trudniejsze do rozważenia niż wątki.

W przypadku asynchronicznych operacji we / wy zawsze można mieć pewność, że dla każdego otwartego deskryptora ścieżka wykonania jest spójna i uporządkowana. Istnieją oczywiście kwestie, którymi należy się zająć, na przykład co zrobić, gdy kod zależny od jednego otwartego kanału dalej zależy od wyników wywołania kodu, gdy inny otwarty kanał zwraca dane.

Jednym z fajnych rozwiązań dla nieblokujących wejść / wyjść opartych na zdarzeniach jest nowa biblioteka Diesel . W tej chwili jest ograniczony do Linuksa, ale jest niezwykle szybki i dość elegancki.

Warto również poświęcić czas na naukę pyevent , opakowania otaczającego wspaniałą bibliotekę libevent, która zapewnia podstawową strukturę programowania opartego na zdarzeniach przy użyciu najszybszej dostępnej metody dla Twojego systemu (określanej w czasie kompilacji).

Jeff Ober
źródło
Re Twisted etc .: Korzystam z biblioteki innej firmy, która zajmuje się obsługą sieci; Chciałbym uniknąć łatania się w nim. Ale nadal się tym zajmę, dzięki.
balpha
2
Nic tak naprawdę nie omija GIL. Ale Python zwalnia GIL podczas operacji I / O. Python zwalnia również GIL podczas „przekazywania” skompilowanym modułom, które są odpowiedzialne za samodzielne pozyskiwanie / wydawanie GIL.
Jeff Ober
2
Aktualizacja jest po prostu zła. Kod Pythona działa dokładnie tak samo w wątku Pythona, jak w QThread. Uzyskujesz GIL, gdy uruchamiasz kod w Pythonie (a następnie Python zarządza wykonaniem między wątkami), zwalniasz go, gdy uruchamiasz kod w C ++. Nie ma żadnej różnicy.
Lukáš Lalinský
1
Nie, chodzi o to, że bez względu na to, jak utworzysz wątek, interpreter Pythona nie przejmuje się. Wszystko, o co mu zależy, to to, że może uzyskać GIL i po instrukcjach X może go zwolnić / odzyskać. Możesz na przykład użyć ctypes, aby utworzyć wywołanie zwrotne z biblioteki C, które zostanie wywołane w osobnym wątku, a kod będzie działał dobrze, nawet nie wiedząc, że jest to inny wątek. W module wątków nie ma nic specjalnego.
Lukáš Lalinský
1
Mówiłeś, czym różni się QThread pod względem blokowania i jak „musisz zarządzać dwoma zestawami logiki do blokowania kodu”. Mówię, że wcale nie jest inaczej. Mogę użyć ctypes i pthread_create, aby rozpocząć wątek, i będzie to działać dokładnie w ten sam sposób. Kod Pythona po prostu nie musi przejmować się GIL.
Lukáš Lalinský
21

Zaletą QThreadjest to, że jest zintegrowany z resztą biblioteki Qt. Oznacza to, że metody obsługujące wątki w Qt będą musiały wiedzieć, w którym wątku działają, i aby przenosić obiekty między wątkami, trzeba będzie ich użyć QThread. Inną przydatną funkcją jest uruchamianie własnej pętli zdarzeń w wątku.

Jeśli uzyskujesz dostęp do serwera HTTP, powinieneś rozważyć QNetworkAccessManager.

Lukáš Lalinský
źródło
1
Poza tym, co skomentowałem na temat odpowiedzi Jeffa Obera, QNetworkAccessManagerwygląda obiecująco. Dzięki.
balpha
14

Zadałem sobie to samo pytanie, pracując dla PyTalk .

Jeśli używasz Qt, musisz użyć, QThreadaby móc korzystać z frameworka Qt, a zwłaszcza systemu sygnału / gniazda.

Dzięki silnikowi sygnału / slotów będziesz mógł rozmawiać z jednego wątku na drugi iz każdą częścią projektu.

Co więcej, nie ma zbytniej kwestii wydajności dotyczącej tego wyboru, ponieważ oba są powiązaniami C ++.

Oto moje doświadczenie z PyQt i wątkiem.

Zachęcam do korzystania QThread.

Natim
źródło
9

Jeff ma kilka dobrych punktów. Tylko jeden główny wątek może wykonywać aktualizacje GUI. Jeśli musisz zaktualizować GUI z poziomu wątku, kolejkowane sygnały połączenia Qt-4 ułatwiają wysyłanie danych między wątkami i zostaną automatycznie wywołane, jeśli używasz QThread; Nie jestem pewien, czy będą, jeśli używasz wątków Pythona, chociaż łatwo jest dodać parametr do connect().

Kaleb Pederson
źródło
5

Nie mogę też polecić, ale mogę spróbować opisać różnice między wątkami CPython i Qt.

Po pierwsze, wątki CPythona nie działają współbieżnie, a przynajmniej nie kod Pythona. Tak, tworzą wątki systemowe dla każdego wątku Pythona, jednak tylko wątek aktualnie posiadający Global Interpreter Lock może działać (rozszerzenia C i kod FFI mogą go ominąć, ale kod bajtowy Pythona nie jest wykonywany, gdy wątek nie przechowuje GIL).

Z drugiej strony mamy wątki Qt, które są w zasadzie wspólną warstwą nad wątkami systemowymi, nie mają globalnej blokady interpretera, a zatem mogą działać współbieżnie. Nie jestem pewien, jak radzi sobie z tym PyQt, jednak jeśli twoje wątki Qt nie wywołują kodu Pythona, powinny być w stanie działać jednocześnie (z wyjątkiem różnych dodatkowych blokad, które mogą być zaimplementowane w różnych strukturach).

W celu dodatkowego dostrojenia można zmodyfikować liczbę instrukcji kodu bajtowego, które są interpretowane przed zmianą własności GIL - niższe wartości oznaczają więcej przełączania kontekstu (i prawdopodobnie wyższą responsywność), ale niższą wydajność na pojedynczy wątek (przełączniki kontekstu mają swój koszt - jeśli spróbuj zmieniać co kilka instrukcji, co nie przyspiesza).

Mam nadzieję, że pomoże to w twoich problemach :)

p_l
źródło
7
Ważne jest, aby tutaj zauważyć: PyQt QThreads przyjmują globalną blokadę interpretera . Cały kod Pythona blokuje GIL, a wszystkie QThreads uruchomione w PyQt będą uruchamiać kod Pythona. (Jeśli tak nie jest, w rzeczywistości nie używasz części PyQt „Py” :). Jeśli zdecydujesz się odroczyć z tego kodu Pythona do zewnętrznej biblioteki C, GIL zostanie wydany, ale to prawda niezależnie od tego, czy używasz wątku Pythona, czy wątku Qt.
kwark
Właśnie to starałem się przekazać, że cały kod Pythona przyjmuje blokadę, ale nie ma to znaczenia dla kodu C / C ++ działającego w oddzielnym wątku
p_l
0

Nie mogę wypowiedzieć się na temat dokładnych różnic między Python i PyQt wątków, ale robiłem to, co próbujesz zrobić za pomocą QThread, QNetworkAcessManageri upewniając się do rozmowy QApplication.processEvents(), gdy wątek jest żywe. Jeśli reakcja GUI jest naprawdę problemem, który próbujesz rozwiązać, to później pomoże.

brianz
źródło
1
QNetworkAcessManagernie wymaga wątku ani processEvents. Wykorzystuje asynchroniczne operacje we / wy.
Lukáš Lalinský
Ups ... tak, używam kombinacji QNetworkAcessManageri httplib2. Mój kod asynchroniczny używa httplib2.
brianz