最近笔者遇到了一个很大的难题,不知道如何把多进程做成图形化控制,难题在于如何实时读取进程进度(进程间如何完成双向通信),探索了很久终于实现了,这里分享一些想法。
首先需要先实现一个简单的pyqt界面,以下是工具介绍:
工具 | 说明 |
---|---|
Pycharm | 2020 |
Qt Designer | PyCharm安装工具 |
pyUIC | PyCharm安装工具 |
pyRCC | PyCharm安装工具 |
以上四个工具的具体环境配置步骤可以参考文章:都啥时候了,还不会 使用pyCharm 按装PyQt5
虽然我们也可以直接编写代码实现简单的界面,但笔者还是推荐使用Qt Designer,这样可以借助工具完成大部分的UI设计,然后再通过代码完成关键功能。
Qt Designer的使用可以参考文章:Qt Designer工具的使用
这里笔者将文件称为demo.ui,保存在项目根目录下,Pycharm中Tools->External Tools->pyUIC即可转换为.py文件
接下来笔者给大家介绍一下PyQt5 GUI界面与代码分离的两种方式,笔者在demo中使用了第二种:
方法一:动态加载UI文件
说明:动态加载.ui文件的形式可以同步更新,直接编辑完.ui文件保存后运行即可看到效果
# 方法一:动态加载 UI文件
import sys
from PyQt5 import uic
from PyQt5.QtWidgets import *
class Win_Main(QMainWindow):
def __init__(self):
# 子类中显示调用父类的__init__()方法即构造函数
super().__init__()
# 用initUI()函数创建程序的GUI界面
self.initUI()
def initUI(self):
# 从文件中加载UI定义
self.ui = uic.loadUi("demo.ui")
if __name__ == '__main__':
app = QApplication([])
win_Main = Win_Main()
win_Main.show()
sys.exit(app.exec_())
方法二:转换为.py文件
说明:每次运行前需要将.ui文件手动转换为.py文件,笔者比较推荐第二种方式,虽然比较麻烦,但可以支持更复杂的实现,涉及到一些复杂的操作和引用时,第一种方式并不能很好的兼容
# 方法二:转换为.py文件
import sys
from PyQt5 import uic
from PyQt5.QtWidgets import *
# Ui_MainWindow是.py文件中生成的类名
class Win_Main(QMainWindow, Ui_MainWindow):
def __init__(self, parent = None):
# 子类中显示调用父类的__init__()方法即构造函数
super(Win_Main, self).__init__(parent)
# 用initUI()函数创建程序的GUI界面
self.initUI()
def initUI(self):
self.setupUi(self)
if __name__ == '__main__':
app = QApplication([])
win_Main = Win_Main()
win_Main.show()
sys.exit(app.exec_())
在遇到一些任务时,我们需要考虑实现方式,IO密集型推荐使用多线程,CPU密集型则推荐使用多进程。原因就因为Python中存在GIL(Global Interpreter Lock)全局解释器锁,目的是为了保存数据安全,但这也导致实际上同一时刻只能运行一个线程。在python多线程中,python解释器按照以下方式运行:
1.设置GIL
2.切换到一个线程执行
3.执行
4.把线程设置为睡眠状态
5.解锁GIL
6.重复以上步骤
因为涉及到频繁地线程切换,CPU密集型任务导致多线程不快反慢。但是在IO密集型程序中,当线程遇到I/O操作时会释放GIL,多线程依旧能大幅度提升程序执行效率。
笔者是因为需要处理百万量级的文本操作,涉及到文本移动、比较等,主要是CPU密集型,所以选择了多进程处理方式,接下来介绍一下多进程的实现方式
多进程实现的四种方式:
(1)os.fork():只能在Linux里面实现
(2)使用multiprocessing模块: 创建Process的实例,传入任务执行函数作为参数
(3)使用multiprocessing模块: 派生Process的子类,重写run方法
(4)使用进程池Pool
四种方式的实例如下:
# os.fork()
# fork函数调用一次,返回两次:在父进程中返回值为子进程id,在子进程中返回值为0
# os.getpid() 获取子进程的进程号
# os.getppid() 获取父进程的进程号
import os
pid=os.fork()
if pid==0:
print("执行子进程,子进程pid={pid},父进程ppid={ppid}".format(pid=os.getpid(),ppid=os.getppid()))
else:
print("执行父进程,子进程pid={pid},父进程ppid={ppid}".format(pid=pid,ppid=os.getpid()))
# Process常用属性与方法:
# name:进程名
# pid:进程id
# start():开启进程
# terminate():终止进程
# join(timeout=None):阻塞进程
# run():自定义子类时重写
# is_alive():判断进程是否存活
# 使用multiprocessing模块: 创建Process的实例,传入任务执行函数作为参数
import os,time
from multiprocessing import Process
def worker():
print("子进程执行>>> pid={0},ppid={1}".format(os.getpid(),os.getppid()))
time.sleep(2)
print("子进程终止>>> pid={0}".format(os.getpid()))
def main():
print("主进程执行>>> pid={0}".format(os.getpid()))
ps=[]
# 创建子进程实例
for i in range(2):
p=Process(target=worker,name="worker"+str(i),args=())
ps.append(p)
# 开启进程
for i in range(2):
ps[i].start()
# 阻塞进程
for i in range(2):
ps[i].join()
print("主进程终止")
if __name__ == '__main__':
main()
# 使用multiprocessing模块: 派生Process的子类,重写run方法
import os,time
from multiprocessing import Process
class MyProcess(Process):
def __init__(self):
Process.__init__(self)
def run(self):
print("子进程开始>>> pid={0},ppid={1}".format(os.getpid(),os.getppid()))
time.sleep(2)
print("子进程终止>>> pid={}".format(os.getpid()))
def main():
print("主进程开始>>> pid={}".format(os.getpid()))
myp=MyProcess()
myp.start()
# myp.join()
print("主进程终止")
if __name__ == '__main__':
main()
#进程池Pool
import os,time
from multiprocessing import Pool
def worker(arg):
print("子进程开始执行>>> pid={},ppid={},编号{}".format(os.getpid(),os.getppid(),arg))
time.sleep(0.5)
print("子进程终止>>> pid={},ppid={},编号{}".format(os.getpid(),os.getppid(),arg))
def main():
print("主进程开始执行>>> pid={}".format(os.getpid()))
ps=Pool(5)
for i in range(10):
# ps.apply(worker,args=(i,)) # 同步执行
ps.apply_async(worker,args=(i,)) # 异步执行
# 关闭进程池,停止接受其它进程
ps.close()
# 阻塞进程
ps.join()
print("主进程终止")
if __name__ == '__main__':
main()
笔者选择的方式——最后笔者是选择了进程池方式去执行文本多任务,demo代码如下:
import sys
from PyQt5 import uic
from PyQt5.QtWidgets import *
class Win_Main(QMainWindow, Ui_MainWindow):
def __init__(self, parent = None):
# 子类中显示调用父类的__init__()方法即构造函数
super(Win_Main, self).__init__(parent)
# 用initUI()函数创建程序的GUI界面
self.initUI()
def initUI(self):
self.setupUi(self)
self.btn.clicked.connect(self.multi)
def work(self,path):
pass
def multi(self):
path = int(self.textEdit.text())
ps.apply_async(work,args=(path,)) # 异步执行
def closeEvent(self, event):
super().closeEvent(event)
po.close() # 关闭进程池,关闭后po不再接收新的请求
po.join() # 等待po进程池中所有进程执行完成后,主进程才继续向下运行
if __name__ == '__main__':
# 定义一个进程池,最大进程数为系统进程数-2
po = Pool(os.cpu_count() - 2)
freeze_support()
# 启动图形界面
app = QApplication([])
win_Main = Win_Main()
win_Main.show()
sys.exit(app.exec_())
现在我们已经可以做到多进程化启动任务,要做到图形化控制进程,还需要做到以下几点:
(1)启动时显示进度条,完成时消失
(2)实时获取进程进度,并展示真实进度
(3)勾选后中止进程,进度条消失
笔者查阅了很多资料,发现要结合GUI做到信息的传递与实时显示是非常困难的,典型的进程通信方式有以下几种:
(1)信号量( semaphore ) : 信号量是一个共享资源访问者的计数器,可以用来控制多个进程对共享资源的并发访问数。它常作为一种锁机制,防止指定数量的进程正在访问共享资源时,其他进程也访问该资源。
(2)信号 ( signal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
(3)管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用,进程的亲缘关系通常是指父子进程关系。
(4)有名管道 (named pipe) : 有名管道也是半双工的通信方式,但它允许无亲缘关系进程间的通信。
(5)消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
(6)共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的,往往与其他通信机制(如信号量)配合使用。
(7)套接字( socket ) : socket也是一种进程间通信机制,与其他通信机制不同的是,它主要用于不同机器间的进程通信,同一机器内的进程通信采用此方式是有些浪费的。
(8) 文件:使用文件进行通信是最简单的一种通信方式,一个进程将结果输出到临时文件,另一个进程从文件中读出来。
笔者尝试了以上好几种方式,但没有实现任务需求,前七种方式与GUI交互信息太复杂,而最后一种文件方式则无法顺畅读写,会遇到打开一个文件无法同时读写的问题,相比于寻找各个方法获取进程进度,笔者认为通过比较文本目前完成行数判断进度更为简单,而且进程池方式中不适合直接杀死进程,更合适的方法是结束通过标志提前中止当前任务,最后笔者找到了一种可行的方式:通过进程共享变量传递必要的参数。
关于共享变量,需要注意的是不能将共享变量和共享锁定义成全局变量然后通过global引用,那样会报错,只能通过进程传递,以下是几种数据结构的声明形式:
# 单值声明方式:typecode是进制类型,value是初始值
# 单值形式取值赋值需要通过get()/set()方法进行,不能取值赋值
# share_var = multiprocessing.Manager().Value(typecode, value)
# 数组声明方式:typecode是数组变量中的变量类型,sequence是数组初始值
# share_var = multiprocessing.Manager().Array(typecode, sequence)
# 字典声明方式
# share_var = multiprocessing.Manager().dict()
# 列表声明方式
share_var = multiprocessing.Manager().list()
share_var.append("start flag")
以下是笔者在实现文本多任务时,使用的共享变量参数:
#共享变量 pro[0:-3]代表进度条的进度 pro[-1]代表第几个子进程
manager = multiprocessing.Manager()
pro = manager.list([0, 0, 0, 0, 0, 0, 0])
# inputFile代表子进程的各个输入文件路径
inputFile = manager.list(['', '', '', '', '', ''])
# outputFile代表子进程的各个输出文件路径
outputFile = manager.list(['', '', '', '', '', ''])
# dead代表任务是否中止或结束 0-初始化状态 1-死亡 2-运行中
dead = manager.list(([0, 0, 0, 0, 0, 0, 0]))
在定义好变量之后,在需要使用时,主进程中可以随时操作它,对于子进程,只需要在主进程中在子进程异步执行时将变量作为参数传递进去,就可以随时改变,这样就可以达到信息共享的效果。例如:
po.apply_async(work, (inputFile, outputFile, i, pro, dead))
在实现了进程通信后,进度条如何出现、进程是否中止、如何获取实际进度相关的参数都可以在使用了进程共享变量的情况下很方便地获取到,笔者以进程池数量为6进行举例。
在异步启动子进程前的第一步就是判断进程池是否已满,若满则应该将信息保存,利用下一步中的定时执行的函数中判断进度条是否释放,若释放则取一条记录启动进程指定并占用进度条,若进程池未满则可以直接修改以下参数,而后启动进度条,然后再异步启动子进程。关于中止的部分,只需要在页面增加一个按钮,点击时修改进程状态,任务函数也根据该状态判断是否提前结束即可。
# 定位进度条
i = pro[-1] % 6
# 检查进度条释放情况
while(dead[i]):
pass
# 占用进度条
dead[i]=2
pro[-1]+=1
增加进度条启动、进度条刷新、进度条结束等函数方法如下:
#初始化并显示进度条(已经在Qt Designer设计好6个进度条,最初始均为水平布局嵌入垂直布局的隐藏状态)
def startPro(self, i, language):
if i == 0:
self.progressBar.setValue(0)
self.checkBox.setVisible(True)
self.checkBox.setChecked(False)
self.progressBar.setVisible(True)
elif i == 1:
self.progressBar_2.setValue(0)
self.checkBox_2.setVisible(True)
self.checkBox_2.setChecked(False)
self.progressBar_2.setVisible(True)
elif i == 2:
self.progressBar_3.setValue(0)
self.checkBox_3.setVisible(True)
self.checkBox_3.setChecked(False)
self.progressBar_3.setVisible(True)
elif i == 3:
self.progressBar_4.setValue(0)
self.checkBox_4.setVisible(True)
self.checkBox_4.setChecked(False)
self.progressBar_4.setVisible(True)
elif i == 4:
self.progressBar_5.setValue(0)
self.checkBox_5.setVisible(True)
self.checkBox_5.setChecked(False)
self.progressBar_5.setVisible(True)
elif i == 5:
self.progressBar_6.setValue(0)
self.checkBox_6.setVisible(True)
self.checkBox_6.setChecked(False)
self.progressBar_6.setVisible(True)
#笔者以进程池数量为6进行举例
def updatePro(self):
self.progressBar.setValue(pro[0])
self.progressBar_2.setValue(pro[1])
self.progressBar_3.setValue(pro[2])
self.progressBar_4.setValue(pro[3])
self.progressBar_5.setValue(pro[4])
self.progressBar_6.setValue(pro[5])
for n in range(6):
if dead[n] == 1:
self.endPro(n,1)
dead[n] = 0
if pro[n] == 100:
self.endPro(n,0)
pro[n] = 0
# 关于进程池任务判断的部分,这里仅说明步骤含义,读者可自行实现代码
# 判断进程池是否未满
pass
# 未满,如果有等待执行的进程就执行work()
pass
#隐藏进度条
def endPro(self,i,w):
if i == 0:
self.checkBox.setVisible(False)
self.checkBox.setChecked(False)
self.progressBar.setVisible(False)
elif i == 1:
self.checkBox_2.setVisible(False)
self.checkBox_2.setChecked(False)
self.progressBar_2.setVisible(False)
elif i == 2:
self.checkBox_3.setVisible(False)
self.checkBox_3.setChecked(False)
self.progressBar_3.setVisible(False)
elif i == 3:
self.checkBox_4.setVisible(False)
self.checkBox_4.setChecked(False)
self.progressBar_4.setVisible(False)
elif i == 4:
self.checkBox_5.setVisible(False)
self.checkBox_5.setChecked(False)
self.progressBar_5.setVisible(False)
elif i == 5:
self.checkBox_6.setVisible(False)
self.checkBox_6.setChecked(False)
self.progressBar_6.setVisible(False)
最后就是需要定时刷新页面读取最新的数据,定时判断是否执行一些实时的操作(笔者把定时任务都集中在了一个函数方法中),这里通过设置定时器来实现。
第一步:initUI函数中设置定时器,指定更新进度条方法定时执行,并启动
def initUI(self):
self.setupUi(self)
self.btn.clicked.connect(self.multi)
self.timer = QTimer(self)
# 每隔0.1s执行一次更新进度条数据
self.timer.timeout.connect(self.updatePro)
self.timer.start(100)
第二步:页面关闭时,计时器停止
def closeEvent(self, event):
super().closeEvent(event)
self.timer.stop() # 关闭计时器
po.close() # 关闭进程池,关闭后po不再接收新的请求
po.join() # 等待po进程池中所有进程执行完成后,主进程才继续向下运行
笔者最后实现了一个多进程方式大批量处理文本的工具,其中就包括启动任务(用户前台操作,任务FIFO式后台执行,杜绝了因单任务执行时间过长而出现“假死”状态的情况),展示多进程进度,中止多进程等控制功能。因为具体结合了其他很多操作,所以文中只给了关于进程控制相关的代码,没有详细的截图说明和整体代码的提供,读者可以根据需求筛选使用,其中还有很多可以优化的部分,欢迎大家给笔者提意见!!!