使用 PyQtQThread 防止凍結 GUI

      網友投稿 976 2022-05-29

      目錄

      使用長時間運行的任務凍結 GUI

      多線程:基礎

      PyQt 中的多線程與 QThread

      主線程

      工作線程

      使用 QThread 與 Python 的線程

      使用 QThread 防止凍結 GUI

      重用線程:QRunnable 和 QThreadPool

      與 Worker QThreads 通信

      使用信號和槽

      在線程之間共享數據

      使用 QMutex 保護共享數據

      PyQt 中的多線程:最佳實踐

      結論

      PyQt 圖形用戶界面 (GUI) 應用程序具有一個運行事件循環和 GUI的主執行線程。如果您在此線程中啟動一個長時間運行的任務,那么您的 GUI 將凍結,直到任務終止。在此期間,用戶將無法與應用程序交互,從而導致糟糕的用戶體驗。幸運的是,PyQt 的類允許您解決這個問題。QThread

      在本教程中,您將學習如何:

      使用 PyQtQThread來防止 GUI 凍結

      創建可重復使用的線程與QThreadPool和QRunnable

      使用信號和槽管理線程間通信

      使用PyQt 的鎖安全地使用共享資源

      使用PyQt 線程支持開發 GUI 應用程序的最佳實踐

      為了更好地理解如何使用 PyQt 的線程,使用 PyQt和Python 多線程編程的一些GUI 編程知識會有所幫助。

      使用長時間運行的任務凍結 GUI

      長時間運行的任務占用 GUI 應用程序的主線程并導致應用程序凍結是 GUI 編程中的一個常見問題,幾乎總是會導致糟糕的用戶體驗。例如,考慮以下 GUI 應用程序:

      假設您需要Counting標簽來反映Click me!上的總點擊次數!按鈕。單擊長時間運行的任務!按鈕將啟動一個需要很長時間才能完成的任務。您長時間運行的任務可能是文件下載、對大型數據庫的查詢或任何其他資源密集型操作。

      這是使用 PyQt 和單個執行線程對此應用程序進行編碼的第一種方法:

      import sys from time import sleep from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget, ) class Window(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.clicksCount = 0 self.setupUi() def setupUi(self): self.setWindowTitle("Freezing GUI") self.resize(300, 150) self.centralWidget = QWidget() self.setCentralWidget(self.centralWidget) # Create and connect widgets self.clicksLabel = QLabel("Counting: 0 clicks", self) self.clicksLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.stepLabel = QLabel("Long-Running Step: 0") self.stepLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.countBtn = QPushButton("Click me!", self) self.countBtn.clicked.connect(self.countClicks) self.longRunningBtn = QPushButton("Long-Running Task!", self) self.longRunningBtn.clicked.connect(self.runLongTask) # Set the layout layout = QVBoxLayout() layout.addWidget(self.clicksLabel) layout.addWidget(self.countBtn) layout.addStretch() layout.addWidget(self.stepLabel) layout.addWidget(self.longRunningBtn) self.centralWidget.setLayout(layout) def countClicks(self): self.clicksCount += 1 self.clicksLabel.setText(f"Counting: {self.clicksCount} clicks") def reportProgress(self, n): self.stepLabel.setText(f"Long-Running Step: {n}") def runLongTask(self): """Long-running task in 5 steps.""" for i in range(5): sleep(1) self.reportProgress(i + 1) app = QApplication(sys.argv) win = Window() win.show() sys.exit(app.exec())

      在這個 Freezing GUI 應用程序中,.setupUi()為GUI創建所有必需的圖形組件。點擊點擊我!按鈕調用.countClicks(),這使得計數標簽的文本反映按鈕點擊次數。

      注意:?PyQt 最初是針對 Python 2 開發的,它有一個exec關鍵字。為了避免 PyQt 早期版本的名稱沖突,在.exec_().

      盡管 PyQt5 僅針對沒有exec關鍵字的Python 3,但該庫提供了兩種方法來啟動應用程序的事件循環:

      .exec_()

      .exec()

      該方法的兩種變體的工作方式相同,因此您可以在應用程序中使用其中一種。

      單擊長時間運行的任務!按鈕調用.runLongTask(),它執行需要5幾秒鐘才能完成的任務。這是您使用 編碼的假設任務time.sleep(secs),它將調用線程的執行暫停給定的秒數,secs。

      在 中.runLongTask(),您還調用.reportProgress()以使Long-Running Step標簽反映操作的進度。

      此應用程序是否按您的預期工作?運行應用程序并檢查其行為:

      當您單擊單擊我!按鈕,標簽顯示點擊次數。但是,如果您單擊長時間運行的任務!按鈕,然后應用程序變得凍結和無響應。按鈕不再響應點擊并且標簽不反映應用程序的狀態。

      五秒鐘后,應用程序的 GUI 再次更新。該計數標簽呈現了十個點擊,反映了5次點擊發生而GUI是凍結的。在長時間運行的步驟標簽不反映你的長期運行的操作的進度。它從零跳到五,不顯示中間步驟。

      注意:即使您的應用程序的 GUI 在長時間運行的任務期間凍結,該應用程序仍會注冊諸如點擊和擊鍵之類的事件。在主線程被釋放之前,它無法處理它們。

      由于主線程阻塞,應用程序的 GUI 凍結。主線程正忙于處理一個長時間運行的任務,并且不會立即響應用戶的操作。這是一個令人討厭的行為,因為用戶不確定應用程序是否正常工作或是否崩潰。

      幸運的是,您可以使用一些技術來解決此問題。一個常用的解決方案是使用工作線程在應用程序的主線程之外運行長時間運行的任務。

      在下面的部分中,您將學習如何使用 PyQt 的內置線程支持來解決 GUI 無響應或凍結的問題,并在您的應用程序中提供最佳的用戶體驗。

      多線程:基礎

      有時,您可以將您的程序分成幾個較小的子程序或任務,這些子程序或任務可以在多個線程中運行。這可能會使您的程序更快,或者它可以通過防止您的程序在執行長時間運行的任務時凍結來幫助您改善用戶體驗。

      甲線程是一個單獨的執行流程。在大多數操作系統中,線程是進程的一個組成部分,進程可以有多個線程同時執行。每個進程代表當前在給定計算機系統中運行的程序或應用程序的一個實例。

      您可以根據需要擁有任意數量的線程。挑戰在于確定要使用的正確線程數。如果您正在使用I/O 綁定線程,那么線程數將受到可用系統資源的限制。另一方面,如果您正在使用受CPU 限制的線程,那么您將受益于線程數量等于或小于系統中 CPU 內核的數量。

      構建能夠使用不同線程運行多個任務的程序是一種稱為多線程編程的編程技術。理想情況下,使用這種技術,多個任務可以同時獨立運行。然而,這并不總是可能的。至少有兩個元素可以阻止程序并行運行多個線程:

      中央處理器(CPU)

      編程語言

      例如,如果您有一臺單核 CPU 的機器,那么您就不能同時運行多個線程。但是,一些單核 CPU 可以通過允許操作系統調度多個線程之間的處理時間來模擬并行線程執行。這使您的線程看起來是并行運行的,即使它們實際上一次只運行一個。

      另一方面,如果您有一臺多核 CPU機器或計算機集群,那么您可能能夠同時運行多個線程。在這種情況下,您的編程語言成為一個重要因素。

      一些編程語言的內部組件實際上禁止了多個線程的真正并行執行。在這些情況下,線程似乎只是并行運行,因為它們利用了任務調度系統。

      由于與線程之間共享資源、同步數據訪問和協調線程執行相關的復雜性,多線程程序通常比單線程程序更難編寫、維護和調試。這可能會導致幾個問題:

      競爭條件是當應用程序的行為由于不可預測的事件順序而變得不確定時。這通常是兩個或多個線程在沒有適當同步的情況下訪問共享資源的結果。例如,如果讀取和寫入操作以錯誤的順序執行,則從不同線程讀取和寫入內存可能導致競爭條件。

      當線程無限期地等待被鎖定的資源被釋放時,就會發生死鎖。例如,如果一個線程鎖定了一個資源并且在使用后沒有解鎖它,那么其他線程將無法使用該資源并無限期地等待。如果線程 A 正在等待線程 B 解鎖資源,而線程 B 正在等待線程 A 解鎖不同的資源,也會發生死鎖。兩個線程都將永遠等待。

      活鎖是兩個或多個線程重復動作以響應彼此的動作的情況。活鎖線程無法在其特定任務上取得進一步進展,因為它們太忙于相互響應。但是,它們并沒有被阻塞或死亡。

      當進程永遠無法訪問完成其工作所需的資源時,就會發生饑餓。例如,如果您有一個無法獲得 CPU 時間訪問權限的進程,則該進程正在耗盡 CPU 時間并且無法完成其工作。

      在構建多線程應用程序時,您需要小心保護您的資源免受并發寫入或狀態修改訪問。換句話說,您需要防止多個線程同時訪問給定資源。

      廣泛的應用程序至少可以通過以下三種方式從使用多線程編程中受益:

      利用多核處理器使您的應用程序更快

      通過將應用程序劃分為更小的子任務來簡化應用程序結構

      通過將長時間運行的任務卸載到工作線程,使您的應用程序保持響應并保持最新狀態

      在Python 的 C 實現(也稱為CPython)中,線程不是并行運行的。CPython 有一個全局解釋器鎖(GIL),這是一種基本上一次只允許一個 Python 線程運行的鎖。

      這會對線程化 Python 應用程序的性能產生負面影響,因為線程之間的上下文切換會產生開銷。但是,Python 中的多線程可以幫助您解決在處理長時間運行的任務時應用程序凍結或無響應的問題。

      PyQt 中的多線程?QThread

      Qt和PyQt提供了自己的基礎設施來使用QThread.?PyQt 應用程序可以有兩種不同的線程:

      主線程

      工作線程

      應用程序的主線程始終存在。這是應用程序及其 GUI 運行的地方。另一方面,工作線程的存在取決于應用程序的處理需求。例如,如果您的應用程序通常運行需要大量時間才能完成的繁重任務,那么您可能希望有工作線程來運行這些任務并避免凍結應用程序的 GUI。

      主線程

      在 PyQt 應用程序中,執行的主線程也稱為GUI 線程,因為它處理所有小部件和其他 GUI 組件。你通過調用.exec()你的QApplication對象來啟動這個線程。主線程運行應用程序的事件循環以及您的 Python 代碼。它還處理您的窗口、對話框以及與主機操作系統的通信。

      默認情況下,在應用程序的主線程中發生的任何事件或任務,包括用戶在 GUI 本身上的事件,都將同步運行,或者一個接一個地運行。因此,如果您在主線程中啟動一個長時間運行的任務,那么應用程序需要等待該任務完成,并且 GUI 變得無響應。

      請務必注意,您必須在 GUI 線程中創建和更新所有小部件。但是,您可以在工作線程中執行其他長時間運行的任務,并使用它們的結果來提供應用程序的 GUI 組件。這意味著 GUI 組件將充當消費者,從執行實際工作的線程中獲取信息。

      工作線程

      您可以在 PyQt 應用程序中根據需要創建任意數量的工作線程。工作線程是輔助執行線程,您可以使用它從主線程卸載長時間運行的任務并防止 GUI 凍結。

      您可以使用QThread.?每個工作線程都可以有自己的事件循環,并支持 PyQt 的信號和槽機制與主線程進行通信。如果您從從QObject特定線程中繼承的任何類創建對象,則稱該對象屬于該線程,或與該線程有親緣關系。它的孩子也必須屬于同一個線程。

      QThread不是線程本身。它是一個操作系統線程的包裝器。真正的線程對象是在您調用時創建的QThread.start()。

      QThread提供高級應用程序編程接口 (?API?) 來管理線程。這個API包含的信號,如.started()和.finished(),當線程開始和結束被發射。它還包括這樣的方法和槽,如.start(),.wait(),.exit(),.quit(),.isFinished(),和.isRunning()。

      與任何其他線程解決方案一樣,QThread您必須保護您的數據和資源免受并發或同時訪問。否則你會面臨很多問題,包括死鎖、數據損壞等等。

      使用QThreadvs Python 的threading

      當談到在 Python 中使用線程時,您會發現 Python標準庫為該threading模塊提供了一致且健壯的解決方案。該模塊提供了一個高級 API,用于在 Python 中進行多線程編程。

      通常,您將threading在 Python 應用程序中使用。但是,如果您使用 PyQt 使用 Python 構建 GUI 應用程序,那么您還有另一種選擇。PyQt 為執行多線程提供了一個完整的、完全集成的、高級的 API。

      你可能想知道,我應該在我的 PyQt 應用程序中使用什么,Python 的線程支持還是 PyQt 的線程支持?答案是視情況而定。

      例如,如果您正在構建一個也有Web 版本的 GUI 應用程序,那么 Python 的線程可能更有意義,因為您的后端根本不會依賴 PyQt。但是,如果您正在構建裸 PyQt 應用程序,那么 PyQt 的線程適合您。

      使用 PyQt 的線程支持提供以下好處:

      線程相關的類與 PyQt 基礎設施的其余部分完全集成。

      工作線程可以有自己的事件循環,從而啟用事件處理。

      可以使用信號和槽進行線程間通信。

      如果您要與庫的其余部分交互,經驗法則可能是使用 PyQt 的線程支持,否則使用 Python 的線程支持。

      使用QThread防止凍結的GUI

      GUI 應用程序中線程的一個常見用途是將長時間運行的任務卸載到工作線程,以便 GUI 保持對用戶交互的響應。在 PyQt 中,您用于QThread創建和管理工作線程。

      根據 Qt 的文檔,有兩種主要方法可以創建工作線程QThread:

      子類化QThread并重新實現.run()。的實現.run()必須包含執行特定任務所需的所有功能。

      實例化 aQThread提供了一個并行事件循環。事件循環允許線程擁有的對象在其插槽上接收信號,這些插槽將在線程內執行。子類化QThread允許應用程序在沒有事件循環的情況下運行并行代碼。

      Qt 社區中存在一些關于這些方法中哪一種最適合創建工作線程的爭論。然而,第一種方法是Qt 社區和維護者推薦的。

      創建工作線程的第一種方法需要以下步驟:

      通過子類化準備一個工作對象QObject,并將您的長時間運行的任務放入其中。

      創建工作類的新實例。

      創建一個新QThread實例。

      通過調用將工作對象移動到新創建的線程中.moveToThread(thread)。

      連接所需的信號和槽以保證線程間通信。

      調用.start()的上QThread對象。

      您可以使用以下步驟將 Freezing GUI 應用程序轉變為響應式 GUI 應用程序:

      from PyQt5.QtCore import QObject, QThread, pyqtSignal # Snip... # Step 1: Create a worker class class Worker(QObject): finished = pyqtSignal() progress = pyqtSignal(int) def run(self): """Long-running task.""" for i in range(5): sleep(1) self.progress.emit(i + 1) self.finished.emit() class Window(QMainWindow): # Snip... def runLongTask(self): # Step 2: Create a QThread object self.thread = QThread() # Step 3: Create a worker object self.worker = Worker() # Step 4: Move worker to the thread self.worker.moveToThread(self.thread) # Step 5: Connect signals and slots self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.worker.progress.connect(self.reportProgress) # Step 6: Start the thread self.thread.start() # Final resets self.longRunningBtn.setEnabled(False) self.thread.finished.connect( lambda: self.longRunningBtn.setEnabled(True) ) self.thread.finished.connect( lambda: self.stepLabel.setText("Long-Running Step: 0") )

      首先,您執行一些必需的導入。然后運行之前看到的步驟。

      在第 1 步中,您創建Worker了 的子類QObject。在 中Worker,您創建了兩個信號,finished并且progress。請注意,您必須將信號創建為類屬性。

      您還創建了一個名為 的方法.runLongTask(),您可以在其中放置執行長時間運行任務所需的所有代碼。在此示例中,您將使用迭代次數的for循環模擬長時間運行的任務5,每次迭代有 1 秒的延遲。循環還會發出progress信號,指示操作的進度。最后,.runLongTask()發出finished信號以指出處理已完成。

      在步驟 2 到 4 中,您創建 的實例QThread,它將提供運行此任務的空間,以及 的實例Worker。您可以通過調用移動你的工人對象的線程.moveToThread()上worker,使用thread作為參數。

      在步驟 5 中,您連接以下信號和插槽:

      線程的started信號給worker的.runLongTask()slot,保證當你啟動線程的時候,.runLongTask()會被自動調用

      工作finished線程的.quit()插槽thread在worker完成其工作后退出的信號

      在finished該信號.deleteLater()在兩個插槽要刪除的對象的工人,當工作完成線程對象

      最后,在第 6 步中,您使用.start().

      線程運行后,您需要進行一些重置以使應用程序的行為一致。您禁用了長時間運行的任務!按鈕以防止用戶在任務運行時單擊它。您還可以將線程的finished信號與啟用長時間運行任務的lambda函數連接起來!線程結束時的按鈕。您的最終連接將重置Long-Running Step標簽的文本。

      如果您運行此應用程序,您將在屏幕上看到以下窗口:

      由于您將長時間運行的任務卸載到工作線程,您的應用程序現在可以完全響應。就是這樣!您已經成功地使用 PyQtQThread解決了您在前幾節中看到的凍結 GUI 問題。

      重用線程:QRunnable和QThreadPool

      如果您的 GUI 應用程序嚴重依賴多線程,那么您將面臨與創建和銷毀線程相關的大量開銷。您還必須考慮在給定系統上可以啟動多少個線程,以便您的應用程序保持高效。幸運的是,PyQt 的線程支持也為您提供了解決這些問題的方法。

      每個應用程序都有一個全局線程池。您可以通過調用獲取對它的引用QThreadPool.globalInstance()。

      注意:盡管使用默認線程池是一個相當普遍的選擇,但您也可以通過實例化來創建自己的線程池QThreadPool,它提供了可重用線程的集合。

      全局線程池通常根據您當前 CPU 中的內核數來維護和管理建議的線程數。它還處理應用程序線程中任務的排隊和執行。池中的線程是可重用的,這可以防止與創建和銷毀線程相關的開銷。

      要創建任務并在線程池中運行它們,您可以使用QRunnable.?此類表示需要運行的任務或代碼段。創建和執行可運行任務的過程包括三個步驟:

      使用要運行的任務的代碼進行子類化QRunnable和重新實現.run()。

      實例化 的子類QRunnable以創建可運行的任務。

      調用QThreadPool.start()與可運行的任務作為參數。

      .run()必須包含手頭任務所需的代碼。.start()在池中的可用線程之一中啟動您的任務的調用。如果沒有可用線程,則將.start()任務放入池的運行隊列中。當一個線程可用時,其中的代碼.run()將在該線程中執行。

      這是一個 GUI 應用程序,展示了如何在代碼中實現此過程:

      import logging 2import random 3import sys 4import time 5 6from PyQt5.QtCore import QRunnable, Qt, QThreadPool 7from PyQt5.QtWidgets import ( 8 QApplication, 9 QLabel, 10 QMainWindow, 11 QPushButton, 12 QVBoxLayout, 13 QWidget, 14) 15 16logging.basicConfig(format="%(message)s", level=logging.INFO) 17 18# 1. Subclass QRunnable 19class Runnable(QRunnable): 20 def __init__(self, n): 21 super().__init__() 22 self.n = n 23 24 def run(self): 25 # Your long-running task goes here ... 26 for i in range(5): 27 logging.info(f"Working in thread {self.n}, step {i + 1}/5") 28 time.sleep(random.randint(700, 2500) / 1000) 29 30class Window(QMainWindow): 31 def __init__(self, parent=None): 32 super().__init__(parent) 33 self.setupUi() 34 35 def setupUi(self): 36 self.setWindowTitle("QThreadPool + QRunnable") 37 self.resize(250, 150) 38 self.centralWidget = QWidget() 39 self.setCentralWidget(self.centralWidget) 40 # Create and connect widgets 41 self.label = QLabel("Hello, World!") 42 self.label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) 43 countBtn = QPushButton("Click me!") 44 countBtn.clicked.connect(self.runTasks) 45 # Set the layout 46 layout = QVBoxLayout() 47 layout.addWidget(self.label) 48 layout.addWidget(countBtn) 49 self.centralWidget.setLayout(layout) 50 51 def runTasks(self): 52 threadCount = QThreadPool.globalInstance().maxThreadCount() 53 self.label.setText(f"Running {threadCount} Threads") 54 pool = QThreadPool.globalInstance() 55 for i in range(threadCount): 56 # 2. Instantiate the subclass of QRunnable 57 runnable = Runnable(i) 58 # 3. Call start() 59 pool.start(runnable) 60 61app = QApplication(sys.argv) 62window = Window() 63window.show() 64sys.exit(app.exec())

      下面是這段代碼的工作原理:

      在第 19 到 28 行,您對要執行的代碼進行子類化QRunnable并重新實現.run()。在這種情況下,您使用通常的循環來模擬長時間運行的任務。調用logging.info()通過在終端屏幕上打印一條消息來通知您操作的進度。

      在第 52 行,您可以獲得可用線程的數量。這個數字將取決于您的特定硬件,通常基于您的 CPU 內核。

      在第 53 行,您更新標簽的文本以反映您可以運行的線程數。

      在第 55 行,您開始一個for循環,該循環在可用線程上進行迭代。

      在第 57 行,您實例化Runnable,將循環變量i作為參數傳遞以標識當前線程。然后你調用.start()線程池,使用你的可運行任務作為參數。

      需要注意的是一些在本教程中使用的例子是很重要的logging.info()一個基本的配置消息打印到屏幕上。您需要這樣做,因為print()它不是線程安全函數,因此使用它可能會導致輸出混亂。幸運的是,中的函數logging是線程安全的,因此您可以在多線程應用程序中使用它們。

      如果您運行此應用程序,您將獲得以下行為:

      當您單擊單擊我!按鈕,應用程序最多啟動四個線程。在后臺終端,應用程序報告每個線程的進度。如果關閉應用程序,則線程將繼續運行,直到它們完成各自的任務。

      QRunnable在 Python 中無法從外部停止對象。要解決此問題,您可以創建一個全局布爾變量,并從您的QRunnable子類內部系統地檢查它以在您的變量變為True.

      使用QThreadPooland 的另一個缺點QRunnable是QRunnable不支持信號和槽,因此線程間通信可能具有挑戰性。

      另一方面,QThreadPool自動管理線程池并處理這些線程中可運行任務的排隊和執行。池中的線程是可重用的,這有助于減少應用程序的開銷。

      與 Worker QThreads 通信

      如果您正在使用 PyQt 進行多線程編程,那么您可能需要在應用程序的主線程和工作線程之間建立通信。這允許您獲得有關工作線程進度的反饋并相應地更新 GUI,將數據發送到您的線程,允許用戶中斷執行,等等。

      PyQt 的信號和槽機制提供了一種在 GUI 應用程序中與工作線程通信的健壯且安全的方式。

      另一方面,您可能還需要在工作線程之間建立通信,例如共享數據緩沖區或任何其他類型的資源。在這種情況下,您需要確保正確保護您的數據和資源免受并發訪問。

      使用信號和槽

      一個線程安全的對象是一個可并發多線程訪問,并保證在有效狀態的對象。PyQt 的信號和槽是線程安全的,因此您可以使用它們來建立線程間通信以及在線程之間共享數據。

      使用 PyQt 的 QThread 防止凍結 GUI

      您可以將線程發出的信號連接到線程內或不同線程內的插槽。這意味著您可以在一個線程中執行代碼作為對同一線程或另一個線程中發出的信號的響應。這在線程之間建立了安全的通信橋梁。

      信號也可以包含數據,因此如果您發出一個包含數據的信號,那么您將在連接到該信號的所有插槽中接收該數據。

      在響應式 GUI應用程序示例中,您使用了信號和插槽機制來建立線程之間的通信。例如,您將工作線程的progress信號連接到應用程序的.reportProgress()插槽。progress持有一個整數值,指示長時間運行的任務的進度,并.reportProgress()接收該值作為參數,以便它可以更新長時間運行的步驟標簽。

      在不同線程中建立信號槽之間的連接是 PyQt 中線程間通信的基礎。在這一點上,您可以嘗試使用一個QToolBar對象而不是Long-Running Step標簽來顯示響應式 GUI 應用程序中使用信號和槽的操作進度,這是一個很好的練習。

      在線程之間共享數據

      創建多線程應用程序通常需要多個線程訪問相同的數據或資源。如果多個線程同時訪問相同的數據或資源,并且其中至少有一個寫入或修改此共享資源,那么您可能會面臨崩潰、內存或數據損壞、死鎖或其他問題。

      至少有兩種方法可以讓您保護數據和資源免受并發訪問:

      使用以下技術避免共享狀態:

      不可變對象

      線程本地存儲

      可重入代碼

      使用以下技術同步對共享狀態的訪問:

      原子操作

      相互排斥

      如果您需要共享資源,那么您應該使用第二種方法。原子操作在單個執行步驟中執行,因此它們不能被其他線程中斷。它們確保在給定時間只有一個線程會修改資源。

      注意:有關 CPython 如何管理原子操作的參考,請查看哪些類型的全局值突變是線程安全的?

      請注意,其他 Python 實現的行為可能有所不同,因此如果您使用不同的實現,請查看其文檔以獲取有關原子操作和線程安全的更多詳細信息。

      互斥是多線程編程中的常見模式。使用鎖保護對數據和資源的訪問,鎖是一種同步機制,通常只允許一個線程在給定時間訪問資源。

      例如,如果線程 A 需要更新一個全局變量,那么它可以獲取對該變量的鎖。這可以防止線程 B 同時訪問該變量。一旦線程 A 完成對變量的更新,它就會釋放鎖,線程 B 就可以訪問該變量。這是基于互斥原則,它通過在訪問數據和資源時讓線程相互等待來強制同步訪問。

      值得一提的是,使用鎖的成本很高,并且會降低應用程序的整體性能。線程同步強制大多數線程等待資源可用,因此您將不再利用并行執行。

      PyQt 提供了一些方便的類來保護資源和數據免受并發訪問:

      QMutex是一個鎖類,允許您管理互斥。您可以鎖定給定線程中的互斥鎖以獲得對共享資源的獨占訪問。一旦互斥鎖被解鎖,其他線程就可以訪問該資源。

      QReadWriteLock類似于QMutex但區分讀和寫訪問。使用這種類型的鎖,您可以允許多個線程同時對共享資源進行只讀訪問。如果一個線程需要寫入資源,那么所有其他線程必須被阻塞,直到寫入完成。

      QSemaphore是QMutex保護一定數量的相同資源的概括。如果一個信號量正在保護n 個資源,而您試圖鎖定n?+ 1 個資源,那么信號量就會被阻塞,從而阻止線程訪問這些資源。

      使用 PyQt 的鎖類,您可以保護您的數據和資源并防止很多問題。下一節顯示了如何QMutex用于這些目的的示例。

      保護共享數據?QMutex

      QMutex常用于多線程 PyQt 應用程序中,以防止多個線程并發訪問共享數據和資源。在本節中,您將編寫一個 GUI 應用程序,該應用程序使用QMutex對象來保護全局變量免受并發寫入訪問。

      要了解如何使用QMutex,您將編寫一個示例來管理一個銀行賬戶,兩個人可以隨時從該賬戶中取款。在這種情況下,您需要保護帳戶余額免受并行訪問。否則,人們最終可能會提取比他們在銀行中更多的錢。

      例如,假設您有一個 100 美元的帳戶。兩個人同時查看可用余額,看到賬戶有100美元。他們每個人都認為他們可以提取 60 美元并在帳戶中留下 40 美元,因此他們繼續進行交易。帳戶中的最終余額將為 -$20,這可能是一個重大問題。

      要對示例進行編碼,您將首先導入所需的模塊、函數和類。您還添加了一個基本logging配置并定義了兩個全局變量:

      import logging import random import sys from time import sleep from PyQt5.QtCore import QMutex, QObject, QThread, pyqtSignal from PyQt5.QtWidgets import ( QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget, ) logging.basicConfig(format="%(message)s", level=logging.INFO) balance = 100.00 mutex = QMutex()

      balance是一個全局變量,您將使用它來存儲銀行帳戶中的當前余額。mutex是一個QMutex對象,您將使用它來防止balance并行訪問。換句話說,使用mutex,您將阻止多個線程同時訪問balance。

      下一步是創建一個子類,QObject其中包含管理如何從銀行賬戶取款的代碼。你會打電話給那個班級AccountManager:

      class AccountManager(QObject): finished = pyqtSignal() updatedBalance = pyqtSignal() def withdraw(self, person, amount): logging.info("%s wants to withdraw $%.2f...", person, amount) global balance mutex.lock() if balance - amount >= 0: sleep(1) balance -= amount logging.info("-$%.2f accepted", amount) else: logging.info("-$%.2f rejected", amount) logging.info("===Balance===: $%.2f", balance) self.updatedBalance.emit() mutex.unlock() self.finished.emit()

      在 中AccountManager,您首先定義兩個信號:

      finished?指示類何時處理其工作。

      updatedBalance指示何時balance更新。

      然后你定義.withdraw().?在此方法中,您執行以下操作:

      顯示一條消息,指出想要取款的人

      使用global語句balance從內部使用.withdraw()

      呼叫.lock()上mutex獲得鎖,保護balance從并行訪問

      檢查賬戶余額是否允許提取手頭的金額

      調用sleep()模擬操作需要一些時間才能完成

      將余額減少所需的金額

      顯示消息以通知交易是否被接受

      發出updatedBalance信號通知余額已更新

      釋放鎖以允許其他線程訪問?balance

      發出finished信號通知操作完成

      此應用程序將顯示如下窗口:

      這是創建此 GUI 所需的代碼:

      class Window(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.setupUi() def setupUi(self): self.setWindowTitle("Account Manager") self.resize(200, 150) self.centralWidget = QWidget() self.setCentralWidget(self.centralWidget) button = QPushButton("Withdraw Money!") button.clicked.connect(self.startThreads) self.balanceLabel = QLabel(f"Current Balance: ${balance:,.2f}") layout = QVBoxLayout() layout.addWidget(self.balanceLabel) layout.addWidget(button) self.centralWidget.setLayout(layout)

      在當前余額標簽顯示帳戶的可用余額。如果您點擊提款!按鈕,然后應用程序將模擬兩個人同時嘗試從帳戶中取款。您將使用線程模擬這兩個人:

      class Window(QMainWindow): # Snip... def createThread(self, person, amount): thread = QThread() worker = AccountManager() worker.moveToThread(thread) thread.started.connect(lambda: worker.withdraw(person, amount)) worker.updatedBalance.connect(self.updateBalance) worker.finished.connect(thread.quit) worker.finished.connect(worker.deleteLater) thread.finished.connect(thread.deleteLater) return thread

      此方法包含為每個人創建線程所需的代碼。在這個例子中,你將線程的started信號連接到工作線程的.withdraw(),所以當線程啟動時,這個方法會自動運行。您還將工作人員的updatedBalance信號連接到名為 的方法.updateBalance()。此方法將使用當前帳戶更新Current Balance標簽balance。

      這是代碼.updateBalance():

      class Window(QMainWindow): # Snip... def updateBalance(self): self.balanceLabel.setText(f"Current Balance: ${balance:,.2f}")

      每當有人提款時,帳戶的余額都會減少所要求的金額。此方法更新當前余額標簽的文本以反映帳戶余額的變化。

      要完成應用程序,您需要創建兩個人并為他們每個人啟動一個線程:

      class Window(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.setupUi() self.threads = [] # Snip... def startThreads(self): self.threads.clear() people = { "Alice": random.randint(100, 10000) / 100, "Bob": random.randint(100, 10000) / 100, } self.threads = [ self.createThread(person, amount) for person, amount in people.items() ] for thread in self.threads: thread.start()

      首先,您將.threads一個實例屬性添加到Window.?此變量將保存一個線程列表,以防止線程在.startThreads()返回后超出范圍。然后您定義.startThreads()為每個人創建兩個人和一個線程。

      在 中.startThreads(),您執行以下操作:

      清除線程.threads如果有的話刪除已已毀線程

      創建一個包含兩個人的字典,Alice和Bob。每個人都會嘗試從銀行賬戶中提取隨機數量的錢

      使用列表理解為每個人創建一個線程,并.createThread()

      在for循環中啟動線程

      有了這最后一段代碼,您就快完成了。您只需要創建應用程序和窗口,然后運行事件循環:

      app = QApplication(sys.argv) window = Window() window.show() sys.exit(app.exec())

      如果您從命令行運行此應用程序,那么您將獲得以下行為:

      后臺終端中的輸出顯示線程工作。QMutex在本示例中使用對象可以保護銀行帳戶余額并同步對其的訪問。這可以防止用戶提取超過可用余額的金額。

      PyQt 中的多線程:最佳實踐

      在 PyQt 中構建多線程應用程序時,您可以應用一些最佳實踐。這是一個非詳盡列表:

      避免在 PyQt 應用程序的主線程中啟動長時間運行的任務。

      使用QObject.moveToThread()和QThread對象來創建工作線程。

      使用QThreadPool和QRunnable如果你需要管理的工作線程池。

      使用信號和槽來建立安全的線程間通信。

      使用QMutex、QReadWriteLock或QSemaphore來防止線程同時訪問共享數據和資源。

      確保在完成線程之前解鎖或釋放QMutex、QReadWriteLock、 或QSemaphore。

      在具有多個return語句的函數中釋放所有可能執行路徑中的鎖。

      不要嘗試從工作線程創建、訪問或更新 GUI 組件或小部件。

      不要嘗試將QObject具有父子關系的 a移動到不同的線程。

      如果您在 PyQt 中使用線程時始終應用這些最佳實踐,那么您的應用程序將不太容易出錯并且更加準確和健壯。您將防止出現數據損壞、死鎖、競爭條件等問題。您還將為您的用戶提供更好的體驗。

      結論

      在 PyQt 應用程序的主線程中執行長時間運行的任務可能會導致應用程序的 GUI 凍結并變得無響應。這是GUI 編程中的常見問題,可能會導致糟糕的用戶體驗。使用PyQtQThread創建工作線程來卸載長時間運行的任務可以有效地解決 GUI 應用程序中的這個問題。

      在本教程中,您學習了如何:

      使用 PyQtQThread來防止 GUI 應用程序凍結

      創建可重用的QThread對象與PyQt的的QThreadPool和QRunnable

      使用信號和槽為間通信在PyQt的

      使用共享資源的安全與PyQt的的鎖類

      您還了解到,適用于多線程編程與PyQt的和其內置的線程支持的一些最佳做法。

      GUI Python 任務調度

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:使用BeautifulSoup庫解析htm、xml文檔
      下一篇:手把手教學:如何設計 SDK
      相關文章
      久久精品国产精品亚洲艾草网| 中文字幕精品亚洲无线码二区| 久久亚洲精品视频| 亚洲成A人片在线观看中文| 精品国产亚洲男女在线线电影 | 日韩精品亚洲人成在线观看| 在线视频亚洲一区| 亚洲国产精品成人AV在线| 中文字幕 亚洲 有码 在线| 亚洲午夜国产精品| 亚洲AV无码一区二区三区DV| 亚洲欧洲在线观看| 老色鬼久久亚洲AV综合| 67pao强力打造67194在线午夜亚洲| 亚洲av无码成人精品国产| 亚洲乱码av中文一区二区| 亚洲制服丝袜中文字幕| 国产亚洲精品资源在线26u| 亚洲日韩涩涩成人午夜私人影院| 亚洲精品国产免费| 亚洲国产一二三精品无码| 亚洲乱码在线卡一卡二卡新区| 亚洲一级Av无码毛片久久精品 | 偷自拍亚洲视频在线观看 | 亚洲精品欧美综合四区| 亚洲精品成人a在线观看| 国产精品亚洲va在线观看| 国产天堂亚洲精品| 亚洲AV伊人久久青青草原| 亚洲AV成人影视在线观看| 亚洲heyzo专区无码综合| 亚洲剧情在线观看| 亚洲中文字幕无码久久2017 | 亚洲av无码国产综合专区| 国产精品亚洲精品爽爽| 亚洲AV无码乱码在线观看裸奔| 精品国产香蕉伊思人在线在线亚洲一区二区 | 久久精品国产亚洲αv忘忧草 | 亚洲AV日韩综合一区尤物| 亚洲精品天堂成人片AV在线播放 | 亚洲一区二区三区免费在线观看 |