- PyQt5のGUIアプリを常駐させていたが,消費メモリが増え続ける.
- 最大メモリ512MBだからやばい&常駐アプリが常にメモリリークとかやばい
- QThreadを勘違いし,スレッド生成しまくっていたのが原因の1つ
- 生成したスレッド内でループ処理させて,こちらは解決(本稿の内容)
- しかしまだ消費メモリが増え続けるようで,それは断念
#消費メモリの確認手段
メモリリーク確認にobjgraphが良さそうなので,これを使う.
objgraphでメモリリークを調査する
objgraphのshow_growthを使うと, リークしているオブジェクトの種類と増加数を標準出力に出力できる.
>>> import objgraph
>>> objgraph.show_growth()
呼び出す度にリークしているオブジェクトの増加数が出力される. show_growthは内部でgc.collectを呼び出しているので, 自分でGCする必要はない.
簡単なGUIアプリで検証.startボタンを押すとサブスレッドを使った無限ループを始め,stopボタンを押すまで乱数を生成し続ける.
#従来の構成
(whileやforではないが)結果的な無限ループをGUI側で構築していた.
startボタン押す→サブスレッド開始→乱数生成してシグナル発信→メインスレッドで受け取り→サブスレッド開始→... というループ.
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) # シグナル発信+乱数データも送る
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ボタンを押されるまで無限ループ(乱数を生成してシグナル発信し続ける),の流れ.
※ループの度にtime.sleep()で1秒ほど待ってやらないとstopボタンを押せない.これは並行処理なのか・・・?
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
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で監視したが,目立った漏れは見られず断念.