例えばボタンをクリックしたときに重い処理を実行する場合など、そのまま処理すると GUI が固まってしまうため、スレッド化してやります。
概要
python の標準モジュールに threading
がありますが、PySide を使っている場合は QThread
を使います。
本質的にはどちらも同じだそうですが、Signal/Slot など、Qt との対話を行うなら QThread
を使ったほうが良いようです。
ちなみに、Qt には QtConcurrent
という QThread
よりも高レベルな API がありますが、こちらは PySide では提供されていないようです。
- https://stackoverflow.com/questions/32378719/qtconcurrent-in-pyside-pyqt
- https://doc.qt.io/qtforpython/overviews/qtconcurrentrun.html
QThread
のドキュメントは以下です。
また、PySide で GUI を止めることなく重い処理をするサンプルとして、以下がありました。
上記のサンプルでは QThread
を継承し、run
をオーバーライドしていますが、このやり方は良くないそうで、moveToThread
を使って処理をスレッドに渡すやり方のほうが一般的なようです。
詳しくは以下で解説されています。
とはいえ、moveToThread
を使った公式のサンプルが見当たらなかったので、知らない人は知らないんじゃないかなぁと思ったり・・。
というわけで、QThread
と moveToThread
使ったスレッド化のサンプルです。
環境
- Python 3.8.10
- PySide 6.2.2.1
サンプルコード
解説は後にして、まずはコード全体です。
import sys, threading
from PySide6.QtWidgets import QApplication, QWidget, QPushButton, QLabel, QVBoxLayout
from PySide6.QtCore import Qt, QObject, QThread, Signal, Slot
import shiboken6
import time
class Worker(QObject):
"""バックグラウンドで処理を行うクラス
"""
countup = Signal(int)
def __init__(self, parent=None):
super().__init__(parent)
self.__is_canceled = False
def run(self):
print(f'worker started, thread_id={threading.get_ident()}')
count = 0
while not self.__is_canceled:
count += 1
self.countup.emit(count)
time.sleep(0.001)
print(f'worker finished, thread_id={threading.get_ident()}')
def stop(self):
self.__is_canceled = True
class MainWindow(QWidget):
"""メインウィンドウ
"""
def __init__(self, parent=None):
super().__init__(parent)
self.__thread = None
layout = QVBoxLayout()
button = QPushButton('Start')
button.clicked.connect(self.__start)
layout.addWidget(button)
button = QPushButton('Stop')
button.clicked.connect(self.__stop)
layout.addWidget(button)
self.__label = QLabel()
layout.addWidget(self.__label)
self.setLayout(layout)
def __start(self):
"""開始
"""
print(f'start, thread_id={threading.get_ident()}')
self.__stop()
self.__thread = QThread()
self.__worker = Worker()
self.__worker.moveToThread(self.__thread) # 別スレッドで処理を実行する
# シグナルスロットの接続(self.__countup をスレッド側で実行させるために Qt.DirectConnection を指定)
self.__worker.countup.connect(self.__countup, type=Qt.DirectConnection)
# スレッドが開始されたら worker の処理を開始する
self.__thread.started.connect(self.__worker.run)
# ラムダ式を使う場合は Qt.DirectConnection を指定
#self.__thread.started.connect(lambda: self.__worker.run(), type=Qt.DirectConnection)
# スレッドが終了したら破棄する
self.__thread.finished.connect(self.__worker.deleteLater)
self.__thread.finished.connect(self.__thread.deleteLater)
# 処理開始
self.__thread.start()
def __stop(self):
"""停止
"""
print(f'stop, thread_id={threading.get_ident()}')
if self.__thread and shiboken6.isValid(self.__thread):
# スレッドが作成されていて、削除されていない
if self.__thread.isRunning() or not self.__thread.isFinished():
print('thread is stopping')
self.__worker.stop()
self.__thread.quit()
self.__thread.wait()
print('thread is stopped')
def __countup(self, count):
"""countup シグナルに対する処理
"""
self.__label.setText(f'count={count}, thread_id={threading.get_ident()}')
def closeEvent(self, event):
"""closeEvent のオーバーライド(ウィンドウを閉じたときにスレッドを終了させる)
"""
self.__stop()
if __name__ == "__main__":
app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(app.exec())
カウントアップした変数の内容を表示するだけのシンプルなコードです。
スロットがどのスレッドで実行されているかわかるように、スレッドIDも表示しています。
簡単な解説
Worker
が別スレッドで行いたい処理ですが、こいつ自身はスレッド云々を意識せず(どこで実行されるかを意識せず)、処理内容のみを責務としています。
どこで実行するかを意識するのは、処理を開始する MainWindow
です。
シグナルスロット接続時の注意
以下の部分です。
self.__worker.countup.connect(self.__countup, type=Qt.DirectConnection)
self.__countup
は MainWindow
内の関数なので、普通に接続するとメインスレッド側で処理が行われてしまいます。
これをスレッド側で処理が行われるようにするには Qt.DirectConnection
を指定する必要があります。countup
シグナルはスレッド側で動いている Worker
で emit されるので、Qt.DirectConnection
を指定することでシグナルを emit するスレッド側での実行を指定することになります。
ここらへん、以下で詳しく解説されています。
- https://qiita.com/hermit4/items/1606750332d1d2685bdb#connect%E3%81%AE%E7%A8%AE%E9%A1%9E
- https://teratail.com/questions/168096
ちなみに、以下のスレッド開始時の処理ですが、
self.__thread.started.connect(self.__worker.run)
self.__worker
は moveToThread
でスレッド側で処理を行うように指定しているので、 明示的に Qt.DirectConnection
を指定しなくてもよいのですが、これを仮に以下のようにラムダ式などを使った場合は、上記と同じ理由で Qt.DirectConnection
を指定する必要があります。
self.__thread.started.connect(lambda: self.__worker.run(), type=Qt.DirectConnection)
例えば、self.__worker.run
に引数を与えて開始したい場合などはこういうケースもあるのではないかと思います。
でもその場合 moveToThread
を使う意味がなくなりそうですが・・・。(実際、moveToThread
を使わなくても上手く動きます。)
終了処理
スレッドが終了したら deleteLater
でオブジェクトの破棄を行っています。
self.__thread.finished.connect(self.__worker.deleteLater)
self.__thread.finished.connect(self.__thread.deleteLater)
また、スレッドを開始したままウィンドウを閉じたときのために、closeEvent
をオーバーライドしてスレッドを停止するようにしています。
オブジェクトの判定
Start ボタンを押したときにスレッドを停止してから再度開始していますが、その際に既にスレッドが破棄されていないかどうか判定しています。
オブジェクトが破棄されているかどうかは shiboken
を使います。
スレッドの終了判定
QThread
に isRunning
と isFinished
という関数がありますが、どっちを使えばいいのかいまいち分からなかったので両方見るようにしました。
ここらへん微妙です・・・。
参考 URL
- https://stackoverflow.com/questions/1595649/threading-in-a-pyqt-application-use-qt-threads-or-python-threads
- https://doc.qt.io/qtforpython/PySide6/QtCore/QThread.html
- https://fereria.github.io/reincarnation_tech/11_PySide/02_Tips/06_qthread_01/
- https://srinikom.github.io/pyside-docs/PySide/QtCore/QThread.html
- https://theolizer.com/cpp-school4/cpp-school4-8/
- https://code.tiblab.net/python/pyside/thread_ui_change
- https://realpython.com/python-pyqt-qthread/
- https://tukunen13.hatenablog.jp/entry/2017/05/25/004652
- https://stackoverflow.com/questions/11328219/how-to-know-if-object-gets-deleted-in-python