LoginSignup
2
7

More than 5 years have passed since last update.

PyQt5 QThreadで勘違いからメモリリークを起こしていた

Last updated at Posted at 2017-09-28
  • PyQt5のGUIアプリを常駐させていたが,消費メモリが増え続ける.
  • 最大メモリ512MBだからやばい&常駐アプリが常にメモリリークとかやばい
  • QThreadを勘違いし,スレッド生成しまくっていたのが原因の1つ
  • 生成したスレッド内でループ処理させて,こちらは解決(本稿の内容)
  • しかしまだ消費メモリが増え続けるようで,それは断念

消費メモリの確認手段

メモリリーク確認にobjgraphが良さそうなので,これを使う.

objgraphでメモリリークを調査する
objgraphのshow_growthを使うと, リークしているオブジェクトの種類と増加数を標準出力に出力できる.
>>> import objgraph
>>> objgraph.show_growth()
呼び出す度にリークしているオブジェクトの増加数が出力される. show_growthは内部でgc.collectを呼び出しているので, 自分でGCする必要はない.

簡単なGUIアプリで検証.startボタンを押すとサブスレッドを使った無限ループを始め,stopボタンを押すまで乱数を生成し続ける.
memory_leak1.jpg

従来の構成

(whileやforではないが)結果的な無限ループをGUI側で構築していた.
startボタン押す→サブスレッド開始→乱数生成してシグナル発信→メインスレッドで受け取り→サブスレッド開始→... というループ.
qthread01.jpg

ML_qthread.py
import time
import random
from PyQt5.QtCore import QThread, pyqtSignal


class ConcurrentlyWorker(QThread):
    finSignal = pyqtSignal(int)  # 扱うデータがある場合は型を指定する

    def run(self):
        print('threading...')
        time.sleep(1)  # 1秒停止
        rand = random.randint(0, 10000)  # 0から1万の範囲で乱数生成
        self.finSignal.emit(rand)  # シグナル発信+乱数データも送る
ML_window.py
import sys
import objgraph
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, \
    QHBoxLayout
from ML_qthread import ConcurrentlyWorker


class Window(QWidget):
    thread = ConcurrentlyWorker()
    stopFlag = 0

    def __init__(self):
        super().__init__()
        self.title = 'memory leak'
        self.initUI()
        # サブスレッドからシグナルを受け取ると動く
        self.thread.finSignal.connect(self.afterThreadFinished)

    def initUI(self):
        self.setWindowTitle(self.title)
        self.setMinimumSize(300, 50)
        mainLayout = QVBoxLayout()

        horLayout = QHBoxLayout()
        self.start_b = QPushButton('Start Loop')
        self.start_b.clicked.connect(self.startButton)
        self.stop_b = QPushButton('Stop Loop')
        self.stop_b.setEnabled(False)
        self.stop_b.clicked.connect(self.stopButton)

        horLayout.addWidget(self.start_b)
        horLayout.addWidget(self.stop_b)

        mainLayout.addLayout(horLayout)
        self.setLayout(mainLayout)

    def startButton(self):
        print('startButton')
        self.start_b.setEnabled(False)
        self.stop_b.setEnabled(True)
        self.stopFlag = 0
        self.loopMethod()

    def loopMethod(self):
        if (self.stopFlag == 0):
            try:
                self.thread.start()  # スレッド開始
            except Exception as e:
                print(e)

    def stopButton(self):
        print('stopButton')
        self.stop_b.setEnabled(False)
        self.stopFlag = 1
        self.start_b.setEnabled(True)

    # シグナルで送られたデータは引数として受け取れる
    def afterThreadFinished(self, signalData):
        print('thread is finished. signal: ', end='')
        print(signalData)
        objgraph.show_growth()
        self.loopMethod()  # またスレッド開始メソッドへ戻ることでループ


if __name__ == '__main__':
    app = QApplication(sys.argv)
    execute = Window()
    execute.show()
    sys.exit(app.exec_())

実行結果

コンソールに表示してみると,毎回メモリリークしている模様.

startButton
threading...
thread is finished. signal: 1162
function              13129    +13129
dict                   8322     +8322
tuple                  8004     +8004
weakref                4713     +4713
wrapper_descriptor     4364     +4364
list                   3485     +3485
getset_descriptor      2719     +2719
set                    2074     +2074
method_descriptor      1750     +1750
type                   1669     +1669
threading...
thread is finished. signal: 1903
dict                           8324        +2
builtin_function_or_method     1099        +2
weakref                        4715        +2
Event                             8        +1
Condition                        12        +1
deque                            39        +1
PyDBAdditionalThreadInfo          5        +1
_DummyThread                      2        +1
ThreadTracer                      3        +1
threading...
thread is finished. signal: 3573
builtin_function_or_method     1126       +27
dict                           8345       +21
deque                            56       +17
Condition                        25       +13
Queue                             5        +4
frame                            25        +2
tuple                          8006        +2
method                          119        +1
weakref                        4716        +1
Event                             9        +1
threading...
thread is finished. signal: 5216
builtin_function_or_method     1134        +8
dict                           8352        +7
deque                            61        +5
Condition                        29        +4
weakref                        4717        +1
Event                            10        +1
Queue                             6        +1
PyDBAdditionalThreadInfo          7        +1
_DummyThread                      4        +1
ThreadTracer                      5        +1
threading...
thread is finished. signal: 2321
builtin_function_or_method     1142        +8
dict                           8359        +7
deque                            66        +5
Condition                        33        +4
weakref                        4718        +1
tuple                          8007        +1
Event                            11        +1
Queue                             7        +1
PyDBAdditionalThreadInfo          8        +1
_DummyThread                      5        +1
  • この構成ではstart()の度にスレッドを生成しまくる?
  • isFinished()して戻り値を見るとTrueだが,スレッドは残っている?
  • Qtリファレンスの「割り当て解除(deallocate)」という怪しいワード
  • 生成したスレッドはdeleteLater()しないとメモリを開放しない?

Qt Documentation

Managing Threads

From Qt 4.8 onwards, it is possible to deallocate objects that live in a thread that has just ended, by connecting the finished() signal to QObject::deleteLater().

改良した構成

GUI側で作っていたループ構成を全部サブスレッドに投げた.
startボタン押す→サブスレッド生成して開始→GUI側のstopボタンを押されるまで無限ループ(乱数を生成してシグナル発信し続ける),の流れ.
qthread02.jpg

※ループの度にtime.sleep()で1秒ほど待ってやらないとstopボタンを押せない.これは並行処理なのか・・・?

noML_qthread.py
import time
import random
from PyQt5.QtCore import QThread, pyqtSignal


class ConcurrentlyWorker(QThread):
    finSignal = pyqtSignal(int)  # 扱うデータがある場合は型を指定する
    loopFlag = 1

    def run(self):
        print('threading...')
        while (True):
            if (self.loopFlag == 1):
                self.doLoop()
            else:
                break

    def doLoop(self):
        print('Loop...')
        rand = random.randint(0, 10000)  # 0から1万の範囲で乱数生成
        self.finSignal.emit(rand)  # シグナル発信+乱数データも送る
        time.sleep(1)  # 1秒停止

    def onLoop(self):
        self.loopFlag = 1

    def offLoop(self):
        self.loopFlag = 0
noML_window.py
import sys
import objgraph
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, \
    QHBoxLayout
from noML_qthread import ConcurrentlyWorker


class Window(QWidget):
    thread = ConcurrentlyWorker()

    def __init__(self):
        super().__init__()
        self.title = 'no memory leak'
        self.initUI()
        # サブスレッドからシグナルを受け取ると動く
        self.thread.finSignal.connect(self.afterThreadFinished)

    def initUI(self):
        self.setWindowTitle(self.title)
        self.setMinimumSize(300, 50)
        mainLayout = QVBoxLayout()

        horLayout = QHBoxLayout()
        self.start_b = QPushButton('Start Loop')
        self.start_b.clicked.connect(self.startButton)
        self.stop_b = QPushButton('Stop Loop')
        self.stop_b.setEnabled(False)
        self.stop_b.clicked.connect(self.stopButton)

        horLayout.addWidget(self.start_b)
        horLayout.addWidget(self.stop_b)

        mainLayout.addLayout(horLayout)
        self.setLayout(mainLayout)

    def startButton(self):
        print('startButton')
        self.start_b.setEnabled(False)
        self.stop_b.setEnabled(True)
        try:
            self.thread.onLoop()
            self.thread.start()  # スレッド開始
        except Exception as e:
            print(e)

    def stopButton(self):
        print('stopButton')
        self.stop_b.setEnabled(False)
        self.thread.offLoop()
        self.start_b.setEnabled(True)

    # シグナルで送られたデータは引数として受け取れる
    def afterThreadFinished(self, signalData):
        print('thread is finished. signal: ', end='')
        print(signalData)
        objgraph.show_growth()  # メモリリークがあれば表示される


if __name__ == '__main__':
    app = QApplication(sys.argv)
    execute = Window()
    execute.show()
    sys.exit(app.exec_())

実行結果

実行直後だけ漏れるが,後はメモリリークしなくなる.

startButton
threading...
Loop...
thread is finished. signal: 7445
function              13131    +13131
dict                   8325     +8325
tuple                  8004     +8004
weakref                4714     +4714
wrapper_descriptor     4364     +4364
list                   3486     +3486
getset_descriptor      2719     +2719
set                    2074     +2074
method_descriptor      1750     +1750
type                   1669     +1669
Loop...
thread is finished. signal: 4893
weakref     4715        +1
Loop...
thread is finished. signal: 4909
Loop...
thread is finished. signal: 4489
Loop...
thread is finished. signal: 7490
builtin_function_or_method     1110       +13
dict                           8334        +9
deque                            46        +8
Condition                        17        +6
frame                            28        +2
Queue                             3        +2
method                          119        +1
Loop...
thread is finished. signal: 2028
Loop...
thread is finished. signal: 8005
Loop...
thread is finished. signal: 1791
Loop...
thread is finished. signal: 6538
Loop...
thread is finished. signal: 5507
Loop...
thread is finished. signal: 6143
Loop...
thread is finished. signal: 7569
Loop...
thread is finished. signal: 1478
Loop...
thread is finished. signal: 7226
Loop...
thread is finished. signal: 6950
Loop...
thread is finished. signal: 6034

まとめ

  • メモリリーク原因の1つは,QThreadの使い方が良くなかったこと
  • それは改善できたが,常駐アプリに適用してもまだメモリリークが続く
  • objgraphで監視したが,目立った漏れは見られず断念.
2
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
7