TKinter GUI库没有像MFC那样的强制安全策略,在MFC中子线程无法直接控制主线程中的GUI控件,比如修改文本框或者按钮上的内容。因为如果只有2个线程,即一个主线程和一个子线程的情况下子线程直接修改主线程控件的相关变量是没什么问题的,也不会出错,但是如果子线程个数大于1,就可能会出现多个线程同时修改一个控件相关变量的情况,导致混乱出错。所以MFC中的办法是自定义一个消息,然后子线程调用这个自定义的消息给主线程发送消息,再由主线程选择响应或者不响应,这样选择权就在主线程,因而可以避免出错。但是Python的TKinter库没这限制,子线程也可以直接访问主线程控件相关变量,也就是说需要用户自己去实现MFC相似的功能,这里就需要用到Python为线程设计的Queue模块,也就是队列。
这个模块可以模拟栈那样先进先出的模式也可以像韩信管谷仓那样搞推陈出新,可以想象成一根管子,把球往管子里塞,最后球从这边口进去会从那边的口出来。用这种方式来接收子线程发送过来的消息,然后在取出消息时判断是哪个线程发送过来的,再把之分配给相应的控件即可。
Queue队列:
下面是例子:
import tkinter as tk
from time import sleep
import queue as Queue
import threading
class GUI():
iolaw = None#申明变量的类型
phelda = None#申明变量的类型
msg_queue = None#创建一个队列
def __init__(self, root):
self.iolaw = tk.StringVar()#申明变量的类型
self.phelda = tk.StringVar()#申明变量的类型
self.msg_queue = Queue.Queue()#创建一个队列
self.initGUI(root)#循环读取队列中的内容刷新控件中的内容
def handee(self,root):
#把队列中的内容取出赋值给label控件
mpica=self.msg_queue.empty()#检查队列是否为空
if(mpica==False):
ontad=self.msg_queue.get()
ourta=ontad.split(",")
if(ourta[0]=="1"):
self.iolaw.set(ourta[1])
else:
if(ourta[0]=="2"):
self.phelda.set(ourta[1])
root.after(500, self.handee,root)#递归调用实现循环,TKinter UI线程中无法使用传统的while循环只能用它这个自带的函数递归实现循环
def hit_me(self):
#点击按钮启动两个子线程
thread = threading.Thread(target=self.line01)
wimme = threading.Thread(target=self.line02)
thread.start()
wimme.start()
def line01(self):
#线程回调函数
for i in range(1,10) :
mmer=str(i)
summes="1,"+mmer
self.msg_queue.put(summes)
sleep(1)
self.msg_queue.put("1,发送完毕")
def line02(self):
#线程回调函数
for i in range(20,30) :
mmer=str(i)
oceanw="2,"+mmer
self.msg_queue.put(oceanw)
sleep(1)
self.msg_queue.put("2,发送完毕")
def initGUI(self, root):
root.geometry('500x300') # 这里的乘是小x
fm1 = tk.Frame(root)
tk.Button(fm1, text="按钮",command=self.hit_me).grid(row=0,column=0,pady=10,sticky=tk.N,columnspan=2)
tk.Label(fm1, textvariable=self.iolaw,bg='green',fg='white',width=20, height=3).grid(row=1,column=0)
tk.Label(fm1, textvariable=self.phelda,bg='red',fg='white',width=20, height=3).grid(row=1,column=1)
fm1.pack()
root.after(100, self.handee,root)
root.mainloop()
if __name__ == "__main__":
root = tk.Tk()
myGUI = GUI(root)
例子中用两个子线程向Queue压入消息,在主线程中取出消息,切割字符串后判断是哪一线程发过来的消息,判断后分发给不同的控件。
多进程版本
大家都知道Python的多线程其实是伪多线程,因为Python代码的执行由Python虚拟机(解释器)来控制。Python在设计之初就考虑要在主循环中,同时只有一个线程在执行,就像单CPU的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在CPU中运行。同样地,虽然Python解释器可以运行多个线程,只有一个线程在解释器中运行。
对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同时只有一个线程在运行。在多线程环境中,Python虚拟机按照以下方式执行。
1.设置GIL。
2.切换到一个线程去执行。
3.运行。
4.把线程设置为睡眠状态。
5.解锁GIL。
6.再次重复以上步骤。
对所有面向I/O的(会调用内建的操作系统C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其他线程在这个线程等待I/O的时候运行。如果某线程并未使用很多I/O操作,它会在自己的时间片内一直占用处理器和GIL。也就是说,I/O密集型的Python程序比计算密集型的Python程序更能充分利用多线程的好处。
我们都知道,比方我有一个4核的CPU,那么这样一来,在单位时间内每个核只能跑一个线程,然后时间片轮转切换。但是Python不一样,它不管你有几个核,单位时间多个核只能跑一个线程,然后时间片轮转。看起来很不可思议?但是这就是GIL搞的鬼。任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
所以如果要实现真·多线程在Python里用的是多进程的方式,不然是无法真正利用多核CPU的并发处理能力的,但是进程和进程之间是隔离的,进程之间的通信就不像多线程那样方便,下面是例子:
from time import sleep
from multiprocessing import Process,Queue
import tkinter as tk
class Gui():
root = tk.Tk()
iolaw = tk.StringVar()#申明变量的类型
phelda = tk.StringVar()#申明变量的类型
ngit= Queue()
def __init__(self):
self.nteres(self.root,self.ngit,self.iolaw,self.phelda)#创建窗口主程序逻辑
def line01(self,q):
#线程回调函数
for i in range(1,10) :
mmer=str(i)
summes="1,"+mmer
q.put(summes)
sleep(1)
q.put("1,发送完毕")
def line02(self,q):
#线程回调函数
for i in range(20,30) :
mmer=str(i)
oceanw="2,"+mmer
q.put(oceanw)
sleep(1)
q.put("2,发送完毕")
def confid(self,q):
p1=Process(target=self.line01,args=(q,)) #创建子进程
p2=Process(target=self.line02,args=(q,))
p1.start()
p2.start()
def itudew(self,root,q,iolaw,phelda):
#读出队列中的信息
mpica=q.empty()#检查队列是否为空
if(mpica==False):
ontad=q.get()
ourta=ontad.split(",")
if(ourta[0]=="1"):
iolaw.set(ourta[1])
else:
if(ourta[0]=="2"):
phelda.set(ourta[1])
root.after(100,self.itudew,root,q,iolaw,phelda)
def nteres(self,root,q,iolaw,phelda):
root.geometry('500x300') # 这里的乘是小x
fm1 = tk.Frame(root)
tk.Button(fm1, text="按钮",command=lambda: self.confid(q)).grid(row=0,column=0,pady=10,sticky=tk.N,columnspan=2)
tk.Label(fm1, textvariable=iolaw,bg='green',fg='white',width=20, height=3).grid(row=1,column=0)
tk.Label(fm1, textvariable=phelda,bg='red',fg='white',width=20, height=3).grid(row=1,column=1)
fm1.pack()
root.after(100, self.itudew,root,q,iolaw,phelda)
root.mainloop()
if __name__ == "__main__":
amerie=Gui()
多进程之间交换数据有好几种办法,在这里鄙人依旧使用Queue容器,只不过这里的Queue不是tk对象下的Queue,而是多进程模块multiprocessing下的Queue。在多进程方案下关于Tk的对象或者说变量不能直接用self.××的方式直接引用,而只能先在类中声明类成员变量(属性),再把变量通过函数参数的方式传入函数内部使用,直接self.××(变量名)的方式引用会报错,但如果是多进程方案择不会。或者也可以在 if __name__ == "__main__": 这一句后声明变量再通过创建类实例的时候通过构造函数传入。
Pipe通道:
这个pipe不同于对文件进行IO操作的那个os.Pipe,此Pipe是multiprocessing跨进程通信模块下的同名Pipe类。
Pipe常用于两个进程,两个进程分别位于管道的两端
Pipe方法返回(conn1,conn2)代表一个管道的两个端,Pipe方法有duplex参数,默认为True,即全双工模式,若为FALSE,conn1只负责接收信息,conn2负责发送,
send和recv方法分别为发送和接收信息。
例:
#!coding:utf-8
import multiprocessing
import os,time,random
#写数据进程执行的代码
def proc_send(pipe,urls):
#发送函数
for url in urls:
print('Process is send :%s' %url)
pipe.send(url)
time.sleep(random.random())
#读数据进程的代码
def proc_recv(pipe):
while True:
print('Process rev:%s' %pipe.recv())
time.sleep(random.random())
if __name__ == '__main__':
#父进程创建pipe,并传给各个子进程
haha,lala = multiprocessing.Pipe()
p1 = multiprocessing.Process(target=proc_send,args=(lala,['url_'+str(i) for i in range(10) ]))
p2 = multiprocessing.Process(target=proc_recv,args=(haha,))
#启动子进程,写入
p1.start()
p2.start()
p1.join()
p2.terminate()
如果只给Pipe一个接收变量这个变量会自动变成一个列表,例如:
#父进程创建pipe,并传给各个子进程
pipe = multiprocessing.Pipe()
p1 = multiprocessing.Process(target=proc_send,args=(pipe[0],['url_'+str(i) for i in range(10) ]))
p2 = multiprocessing.Process(target=proc_recv,args=(pipe[1],))
参考资料:
Python GUI之tkinter窗口视窗教程大集合(看这篇就够了)
Python进阶:聊聊IO密集型任务、计算密集型任务,以及多线程、多进程