在扫雷游戏中,需要一个秒表,用来记录扫完所有雷所用的秒数。要求扫雷和记时两部分程序互不干扰。这就要用到多线程概念。现代操作系统都是多任务的操作系统,即允许同时运行多个程序。但计算机只有一个CPU,在某时刻只能处理一个任务。操作系统把CPU运行时间分为多个时间片,分给各个任务。由于现代CPU运行速度非常快,而人的反应速度相对于CPU是非常慢的,在感觉上似乎多个任务在同时运行。而在计算机中运行的程序,有时也有多个工作需要同时做,办法就是把程序分到的时间片再分为多个子时间片,分给各个工作,使各个工作能同时进行。这就是多线程的概念的粗浅解释。更多有关多线程概念参见网页: https://www.cnblogs.com/luyuze95/p/11289143.html 在扫雷游戏中,扫雷是主要工作(主程序),占用主线程,再创建一个子线程,运行秒表代码,就能达到两工作同时进行,互不干扰。使用多线程必须引入threading类。还用到time类的延时函数sleep函数,还要引入time类。运行界面如下。 代码如下。
import threading import time import tkinter as tk #事件(Event)对象是线程之间通信的最简单机制之一:一个线程发出事件信号,而其他线程等待该信号。 event = threading.Event() #创建Event对象,用于主程序(主线程)控制子线程 def run(n): #n是创建线程对象时,传递给run()方法的参数值,本程序没用到该参数 event.set() #set()将flag设置为True global k #子线程可以和主线程共用全局变量 while event.is_set(): #flag为True,每隔1秒,k+1,并在label中显示。 time.sleep(1) k+=1 label['text']=str(k) def do_job(): #停止按钮事件函数 event.clear() #clear()将flag设置为False,run函数将退出,子线程结束 root = tk.Tk() root.geometry("300x100") k=0 #k是一个全局变量 label=tk.Label(root,text='0',bd='5',fg='red',font=("Arial",15)) label.place(x=10,y=5,width=40,height=30) button1=tk.Button(root,text='停止',command=do_job,fg='red',font=("Arial",15)) button1.place(x=100,y=5,width=60,height=30) #创建线程对象。调用start()方法后,将调用参数1指定的run()方法在此线程中运行,退出run()方法,线程结束。 t = threading.Thread(target=run, args=(0,)) #参数2为元组,是传递给run()方法的参数值,无参数传递可去掉第2个参数 #setDaemon(True)把所有的子线程都变成了主线程的守护线程,因此当主进程结束后,子线程也会随之结束。 t.setDaemon(True) #如不加此条语句,在计数未停止前,即线程未结束,关闭窗口,会抛出异常 t.start() #激活线程,调用run()方法,退出run()方法,线程结束。 root.mainloop()网上有定时器代码如下:
from threading import Timer def hello(): print ("hello, world") Timer(1, hello).start() t = Timer(1, hello) t.start()对这个定时器,网上有不同看法,有人认为是“粗陋的循环定时器”,认为其有点问题。每一次循环间隔操作,创建一个Timer对象,系统都要创建一个线程,然后再回收,这对系统来说开销很大。如果时间间隔 interval 很短,系统会一下子创建很多线程,这些线程很难快速回收,导致系统内存和cpu资源被消耗掉。 所以不提倡在 function 里继续注册一个 Timer。有人觉得是递归调用,会不会由于堆栈溢出,使系统崩溃。但是事实是,将时间间隔改为1秒,运行很长时间,也未崩溃。为了验证函数hello()是在子线程运行,把这段代码移植到tkinter窗口,如函数hello()不是在子线程运行,窗口无法工作。代码如下,运行的很好。这说明这段代码应无问题。我试着解释一下,不对请指正。首先,这不是递归调用,下边代码第7条语句为新线程指定运行函数,执行第9条语句函数就退出了,旧子线程也就结束,很容易被回收了。
from threading import Timer import tkinter as tk def hello(): global k,t k+=1 label['text']=str(k) t=Timer(1, hello) t.setDaemon(True) t.start() def do_job(): t.cancel() root = tk.Tk() root.geometry("300x100") k=0 label=tk.Label(root,text='0',bd='5',fg='red',font=("Arial",15)) label.place(x=10,y=5,width=40,height=30) button1=tk.Button(root,text='停止',command=do_job,fg='red',font=("Arial",15)) button1.place(x=100,y=5,width=60,height=30) t = Timer(1, hello) t.start() root.mainloop()第1段代码只创建了一个子线程,第2段代码创建新线程,旧线程结束,似乎第2段代码好理解些。两段代码优略请大家评判。 关于线程的运行函数退出,线程就结束了观点出自网页: https://docs.python.org/zh-cn/3.7/library/threading.html 涉及的文字如下,不知是否理解正确。 当线程对象一但被创建,其活动一定会因调用线程的 start() 方法开始。这会在独立的控制线程调用 run() 方法。 一旦线程活动开始,该线程会被认为是 ‘存活的’ 。当它的 run() 方法终结了(不管是正常的还是抛出未被处理的异常),就不是’存活的’。 is_alive() 方法用于检查线程是否存活。
