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

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

#消費メモリの確認手段
メモリリーク確認にobjgraphが良さそうなので，これを使う．
>[objgraphでメモリリークを調査する](http://qiita.com/yamaneko1212/items/c17181f7a577bb3b01e9)
objgraphのshow_growthを使うと, リークしているオブジェクトの種類と増加数を標準出力に出力できる.
\>>> import objgraph
\>>> objgraph.show_growth()
呼び出す度にリークしているオブジェクトの増加数が出力される. show_growthは内部でgc.collectを呼び出しているので, 自分でGCする必要はない.


簡単なGUIアプリで検証．startボタンを押すとサブスレッドを使った無限ループを始め，stopボタンを押すまで乱数を生成し続ける．
![memory_leak1.jpg](https://qiita-image-store.s3.amazonaws.com/0/201962/d5e0c207-d446-aed3-5dc2-8740383e49a7.jpeg)


#従来の構成
（whileやforではないが）結果的な無限ループをGUI側で構築していた．
startボタン押す→サブスレッド開始→乱数生成してシグナル発信→メインスレッドで受け取り→サブスレッド開始→... というループ．
![qthread01.jpg](https://qiita-image-store.s3.amazonaws.com/0/201962/f7420961-1437-7a01-9cd0-fe12a51171d7.jpeg)


```python3: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)  # シグナル発信＋乱数データも送る
```

```python3: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](https://doc.qt.io/qt-5/qthread.html)
####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](https://qiita-image-store.s3.amazonaws.com/0/201962/7e342aab-e240-c6f1-4ce5-806362bf4040.jpeg)


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

```python3: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
```

```python3: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で監視したが，目立った漏れは見られず断念．