20

質問:

  1. GUI をロックせずに (「応答なし」) スレッドの進行状況を追跡するためのベスト プラクティスは何ですか?
  2. 一般に、GUI 開発に適用されるスレッド化のベスト プラクティスは何ですか?

質問の背景:

  • Windows 用の PyQt GUI があります。
  • HTMLドキュメントのセットを処理するために使用されます。
  • 一連のドキュメントを処理するには、3 秒から 3 時間かかります。
  • 複数のセットを同時に処理できるようにしたい。
  • GUI をロックしたくありません。
  • これを達成するためにスレッドモジュールを見ています。
  • 私はスレッド化に比較的慣れていません。
  • GUI には進行状況バーが 1 つあります。
  • 選択したスレッドの進行状況を表示したい。
  • 選択したスレッドが終了した場合、そのスレッドの結果を表示します。
  • Python 2.5 を使用しています。

私の考え:進行状況が更新されたときに、進行状況バーを更新する関数をトリガーするQtSignalをスレッドに発行させます。また、結果を表示できるように、処理が終了したときに通知します。

#NOTE: this is example code for my idea, you do not have
#      to read this to answer the question(s).

import threading
from PyQt4 import QtCore, QtGui
import re
import copy

class ProcessingThread(threading.Thread, QtCore.QObject):

    __pyqtSignals__ = ( "progressUpdated(str)",
                        "resultsReady(str)")

    def __init__(self, docs):
        self.docs = docs
        self.progress = 0   #int between 0 and 100
        self.results = []
        threading.Thread.__init__(self)

    def getResults(self):
        return copy.deepcopy(self.results)

    def run(self):
        num_docs = len(self.docs) - 1
        for i, doc in enumerate(self.docs):
            processed_doc = self.processDoc(doc)
            self.results.append(processed_doc)
            new_progress = int((float(i)/num_docs)*100)
            
            #emit signal only if progress has changed
            if self.progress != new_progress:
                self.emit(QtCore.SIGNAL("progressUpdated(str)"), self.getName())
            self.progress = new_progress
            if self.progress == 100:
                self.emit(QtCore.SIGNAL("resultsReady(str)"), self.getName())
    
    def processDoc(self, doc):
        ''' this is tivial for shortness sake '''
        return re.findall('<a [^>]*>.*?</a>', doc)


class GuiApp(QtGui.QMainWindow):
    
    def __init__(self):
        self.processing_threads = {}  #{'thread_name': Thread(processing_thread)}
        self.progress_object = {}     #{'thread_name': int(thread_progress)}
        self.results_object = {}      #{'thread_name': []}
        self.selected_thread = ''     #'thread_name'
        
    def processDocs(self, docs):
        #create new thread
        p_thread = ProcessingThread(docs)
        thread_name = "example_thread_name"
        p_thread.setName(thread_name)
        p_thread.start()
        
        #add thread to dict of threads
        self.processing_threads[thread_name] = p_thread
        
        #init progress_object for this thread
        self.progress_object[thread_name] = p_thread.progress  
        
        #connect thread signals to GuiApp functions
        QtCore.QObject.connect(p_thread, QtCore.SIGNAL('progressUpdated(str)'), self.updateProgressObject(thread_name))
        QtCore.QObject.connect(p_thread, QtCore.SIGNAL('resultsReady(str)'), self.updateResultsObject(thread_name))
        
    def updateProgressObject(self, thread_name):
        #update progress_object for all threads
        self.progress_object[thread_name] = self.processing_threads[thread_name].progress
        
        #update progress bar for selected thread
        if self.selected_thread == thread_name:
            self.setProgressBar(self.progress_object[self.selected_thread])
        
    def updateResultsObject(self, thread_name):
        #update results_object for thread with results
        self.results_object[thread_name] = self.processing_threads[thread_name].getResults()
        
        #update results widget for selected thread
        try:
            self.setResultsWidget(self.results_object[thread_name])
        except KeyError:
            self.setResultsWidget(None)

このアプローチに関するコメント (欠点、落とし穴、賞賛など) を歓迎します。

解像度:

QThread クラスと関連するシグナルとスロットを使用して、スレッド間で通信することになりました。これは主に、私のプログラムが既に GUI オブジェクト/ウィジェットに Qt/PyQt4 を使用しているためです。このソリューションでは、既存のコードに実装する変更も少なくて済みます。

Qt がスレッドとシグナルを処理する方法を説明している該当する Qt の記事へのリンクは次のとおりです ( http://www.linuxjournal.com/article/9602 ) 。以下抜粋:

幸いなことに、Qt では、スレッドが独自のイベント ループを実行している限り、シグナルとスロットをスレッド間で接続できます。これは、重要なアプリケーションで必要になるすべてのブックキーピングおよび中間の QEvent 派生クラスを回避するため、イベントの送受信に比べてはるかにクリーンな通信方法です。スレッド間の通信は、あるスレッドから別のスレッドのスロットに信号を接続することになり、スレッド間でデータを交換する際のミューテックスとスレッド セーフの問題は Qt によって処理されます。

シグナルを接続する各スレッド内でイベント ループを実行する必要があるのはなぜですか? その理由は、あるスレッドから別のスレッドのスロットに信号を接続するときに Qt が使用するスレッド間通信メカニズムに関係しています。このような接続が確立されると、キュー接続と呼ばれます。キューに入れられた接続を介してシグナルが発行されると、宛先オブジェクトのイベントループが次に実行されるときにスロットが呼び出されます。代わりにスロットが別のスレッドからのシグナルによって直接呼び出された場合、そのスロットは呼び出しスレッドと同じコンテキストで実行されます。通常、これはあなたが望むものではありません (特に、データベース接続を使用している場合、データベース接続はそれを作成したスレッドだけが使用できるため、望ましくありません)。キューに入れられた接続は、シグナルをスレッド オブジェクトに適切にディスパッチし、イベント システムをピギーバックすることによって、独自のコンテキストでそのスロットを呼び出します。これはまさに、一部のスレッドがデータベース接続を処理するスレッド間通信に必要なものです。Qt のシグナル/スロット メカニズムは、上記で概説したスレッド間イベント パッシング スキームの実装の根底にありますが、よりクリーンで使いやすいインターフェイスを備えています。

注: elibenにも適切な回答があります。スレッド セーフとミューテックスを処理する PyQt4 を使用していなければ、彼の解決策が私の選択だったでしょう。

4

6 に答える 6

9

シグナルを使用してメイン スレッドへの進行状況を示したい場合は、実際には Python のスレッド モジュールの Thread クラスではなく、PyQt の QThread クラスを使用する必要があります。

QThread、シグナル、およびスロットを使用する簡単な例は、PyQt Wiki にあります。

https://wiki.python.org/moin/PyQt/Threading,_Signals_and_Slots

于 2009-02-22T01:33:00.037 に答える
5

キュー get() でブロックする必要があるため、ネイティブの Python キューは機能しません。これにより、UI が機能しなくなります。

Qt は基本的に、クロス スレッド通信のためにキューイング システムを内部に実装します。スロットへの呼び出しを投稿するには、任意のスレッドからこの呼び出しを試してください。

QtCore.QMetaObject.invokeMethod()

これは扱いにくく、文書化も不十分ですが、Qt 以外のスレッドからでも、必要なことを行う必要があります。

これにはイベント機構を使用することもできます。「post」などの名前のメソッドについては、QApplication (または QCoreApplication) を参照してください。

編集:これはより完全な例です...

QWidget に基づいて独自のクラスを作成しました。文字列を受け入れるスロットがあります。私は次のように定義しています。

@QtCore.pyqtSlot(str)
def add_text(self, text):
   ...

その後、メインの GUI スレッドでこのウィジェットのインスタンスを作成します。メインの GUI スレッドまたはその他のスレッド (ノック オン ウッド) から、次のように呼び出すことができます。

QtCore.QMetaObject.invokeMethod(mywidget, "add_text", QtCore.Q_ARG(str,"hello world"))

不格好ですが、そこに着きます。

ダン。

于 2009-06-19T21:21:59.003 に答える
4

シグナリングの代わりにキューを使用することをお勧めします。個人的には、より同期的であるため、はるかに堅牢で理解しやすいプログラミング方法だと思います。

スレッドはキューから「ジョブ」を取得し、結果を別のキューに戻す必要があります。ただし、スレッドは3番目のキューを使用して、エラーや「進行状況レポート」などの通知やメッセージを送信できます。このようにコードを構造化すると、管理がはるかに簡単になります。

このように、単一の「ジョブキュー」と「結果キュー」をワーカースレッドのグループで使用することもでき、スレッドからのすべての情報をメインGUIスレッドにルーティングします。

于 2009-02-21T07:18:06.127 に答える
1

メソッド「processDoc」が他のデータを変更しない場合(一部のデータを検索して返し、親クラスの変数やプロパティを変更しない場合)、Py_BEGIN_ALLOW_THREADSおよびPy_END_ALLOW_THREADSマクロ(詳細はこちらを参照)を使用できます。そのため、ドキュメントはインタープリターをロックしないスレッドで処理され、UIが更新されます。

于 2009-02-21T05:44:14.730 に答える
0

Python では常にこの問題が発生します。詳細な背景については、Google GIL「グローバルインタープリターロック」。発生している問題を回避するには、一般的に推奨される 2 つの方法があります。Twistedを使用するか、2.5 で導入されたmultiprocessingモジュールに似たモジュールを使用します。

Twisted では非同期プログラミングのテクニックを学ぶ必要があります。最初は混乱するかもしれませんが、スループットの高いネットワーク アプリを作成する必要が生じた場合に役立ち、長期的にはより有益になります。

マルチプロセッシング モジュールは、新しいプロセスをフォークし、IPC を使用して、真のスレッド化があるかのように動作させます。唯一の欠点は、Python 2.5 をインストールする必要があることです。これはかなり新しく、ほとんどの Linux ディストリビューションまたは OSX にデフォルトで含まれています。

于 2009-02-20T18:43:44.160 に答える