前言
在测试利用python调用AI模型API生成图像的程序时候,发现AI模型生成图像有一定的时间,此时UI界面会卡顿,就想到利用多线程来处理,然后就发现多线程之间的数据传递问题,在网络上搜索了相关资料后,一番折腾总算搞清楚了,于是本文作为一个记录,主要是为了以后遇到同样的问题,可以方便参考,当然,如果你有同样的需求,也可以参考本文。
实际需求:最近在使用智谱AI的图像大模型,集成到python中,使用PyQt5作为UI界面,然后就遇到了线程间数据传递的问题。
示例:在主界面添加两个按钮、一个文本框,两个按钮分别表示更新开始和更新复位,文本框实时更新进度值,进度值是利用循环来模拟。
不使用线程
先来看一下不使用子线程的示例:
代码:
import sys
import threading
import time
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class Test11(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.btn0=QPushButton('更新开始',self)
self.btn0.setGeometry(100,100,80,40)
self.btn0.clicked.connect(self.countstart_func)
self.btn1=QPushButton('更新复位',self)
self.btn1.setGeometry(100,160,80,40)
self.btn1.clicked.connect(self.countstop_func)
self.te1=QTextEdit(self)
self.te1.setGeometry(200,100,80,20)
self.te1.setText('0')
self.setWindowTitle('线程数据传递示例')
self.setGeometry(100,100,400,400)
self.show()
def countstart_func(self):
# thread1=threading.Thread(name='th1',target=self.counter_func)
# thread1.start()
self.counter_func()
def counter_func(self):
"""更新进度值"""
for i in range(100):
time.sleep(0.05)
self.te1.setText(str(i))
def countstop_func(self):
"""重置进度值"""
self.te1.setText('0')
if __name__ == '__main__':
app=QApplication(sys.argv)
tt=Test11()
sys.exit(app.exec_())
演示:
通过上面的演示可以发现,点击“更新开始”按钮后,界面就卡住了,然后等待一段时间后,文本框的数值变成99,这期间文本框无法实时更新数值。
那么,我们使用线程来看看:
直接使用线程
import sys
import threading
import time
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class Test11(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.btn0=QPushButton('更新开始',self)
self.btn0.setGeometry(100,100,80,40)
self.btn0.clicked.connect(self.countstart_func)
self.btn1=QPushButton('更新复位',self)
self.btn1.setGeometry(100,160,80,40)
self.btn1.clicked.connect(self.countstop_func)
self.te1=QTextEdit(self)
self.te1.setGeometry(200,100,80,20)
self.te1.setText('0')
self.setWindowTitle('线程数据传递示例')
self.setGeometry(100,100,400,400)
self.show()
def countstart_func(self):
thread1=threading.Thread(name='th1',target=self.counter_func)
thread1.start()
#self.counter_func()
def counter_func(self):
"""更新进度值"""
for i in range(100):
time.sleep(0.05)
self.te1.setText(str(i))
def countstop_func(self):
"""重置进度值"""
self.te1.setText('0')
if __name__ == '__main__':
app=QApplication(sys.argv)
tt=Test11()
sys.exit(app.exec_())
可以看到,上面的代码中,调用循环函数时,新建了一个线程,来运行一下看看:
可以看到,程序运行后,点击“更新开始”按钮,程序直接报错了:
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QTextDocument(0x27bf228b2f0), parent's thread is QThread(0x27bf027dd10), current thread is QThread(0x27bf2ad45e0)
报错的解释:
这个错误信息表明你试图在一个线程中创建一个子对象,而该子对象的父对象位于另一个线程中。在Qt中,对象的父对象和子对象必须在同一个线程中创建和管理,这是为了确保线程安全。
也就是说,直接在新建线程中更新文本框,属于是两个线程间的操作,是不支持的,所以需要采取其他方法。
利用信号、槽在线程间传递数据
既然不能直接在两个线程间操作数据,那就需要间接来对数据进行传递:
import sys
import threading
import time
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class countcount(QThread):
data1=pyqtSignal(str)
def run(self):
self.counter_func()
def counter_func(self):
"""更新进度值"""
for i in range(100):
time.sleep(0.05)
self.data1.emit(str(i))
class Test11(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.btn0=QPushButton('更新开始',self)
self.btn0.setGeometry(100,100,80,40)
self.btn0.clicked.connect(self.countstart_func)
self.btn1=QPushButton('更新复位',self)
self.btn1.setGeometry(100,160,80,40)
self.btn1.clicked.connect(self.countstop_func)
self.te1=QTextEdit(self)
self.te1.setGeometry(200,100,80,20)
self.te1.setText('0')
self.setWindowTitle('线程数据传递示例')
self.setGeometry(100,100,400,400)
self.show()
def countstart_func(self):
self.thread1=countcount()
self.thread1.data1.connect(self.func1)
self.thread1.start()
#self.counter_func()
@pyqtSlot(str)
def func1(self,data):
self.te1.setText(data)
def countstop_func(self):
"""重置进度值"""
self.te1.setText('0')
if __name__ == '__main__':
app=QApplication(sys.argv)
tt=Test11()
sys.exit(app.exec_())
可以看到,代码进行较大的改变,首先是新建了一个线程类:
class countcount(QThread):
data1=pyqtSignal(str)
def run(self):
self.counter_func()
def counter_func(self):
"""更新进度值"""
for i in range(100):
time.sleep(0.05)
self.data1.emit(str(i))
在这个countcount类中定义了一个信号data1,被定义为字符类型的数据,用于向外界传递线程内部的数据,此例中,传递的就是循环中间的每一个数值。
另外,要注意到,原本写在主线程程序中的循环函数,被放到了自定义的线程类中,就是counter_func这个函数。
然后在主线程的调用:
def countstart_func(self):
self.thread1=countcount()
self.thread1.data1.connect(self.func1)
self.thread1.start()
#self.counter_func()
@pyqtSlot(str)
def func1(self,data):
self.te1.setText(data)
在主线程中,将之前自定义的线程类countcount实例化为self.thread1,然后将自定义线程中定义的信号data1与主线程的函数self.func1连接起来。
即,每当data1这个信号触发时:
self.data1.emit(str(i))
都会调用self.func1函数,并且将data1的值传递给func1。
@pyqtSlot(str)
def func1(self,data):
self.te1.setText(data)
在本例中,data1就是循环中的实时值,然后每更新一次,就会调用func1,func1中会将data1的值传到文本框中。
这样就实现了子线程和主线程之间的数据传递。
演示:
完整代码:
import sys
import threading
import time
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class countcount(QThread):
data1=pyqtSignal(str)
def run(self):
self.counter_func()
def counter_func(self):
"""更新进度值"""
for i in range(100):
time.sleep(0.05)
self.data1.emit(str(i))
class Test11(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.btn0=QPushButton('更新开始',self)
self.btn0.setGeometry(100,100,80,40)
self.btn0.clicked.connect(self.countstart_func)
self.btn1=QPushButton('更新复位',self)
self.btn1.setGeometry(100,160,80,40)
self.btn1.clicked.connect(self.countstop_func)
self.te1=QTextEdit(self)
self.te1.setGeometry(200,100,80,20)
self.te1.setText('0')
self.setWindowTitle('线程数据传递示例')
self.setGeometry(100,100,400,400)
self.show()
def countstart_func(self):
self.thread1=countcount()
self.thread1.data1.connect(self.func1)
self.thread1.start()
#self.counter_func()
@pyqtSlot(str)
def func1(self,data):
self.te1.setText(data)
def countstop_func(self):
"""重置进度值"""
self.te1.setText('0')
if __name__ == '__main__':
app=QApplication(sys.argv)
tt=Test11()
sys.exit(app.exec_())
注:最后再说明一下,本文主要是为了作一个记录,方便以后遇到类似问题时,可以直接参考,但如果有同样需求或者问题的朋友,你能够通过此文解决一些问题,那也是很好的。