用Python创造一门标记语言并渲染(5)——线程
- 引言
- 为什么是线程而不是进程
- 使用拓展还是自己敲
- 多线程代码
- 解决思路
- 正确代码
- 结语
引言
经过前面几篇文章,一个标记语言(Tin)的基本框架、渲染的基本思路已经阐述完毕,Tin已经能够被正常地解析渲染了。那么接下来我们就应该开开心心地使用TinEngine做我们想做的事(事实上一直到TinEngine-2.2.0-之前我都没考虑),但总有那么不顺心的情况发生。
当使用耗时标签时(特别是“stop”标签),整个程序界面就会被卡住,当延时暂停时间达到10s(不一定是这个时间)以上时,界面标题会在后面显示“(未响应)”,读者(用户)还以为是不是程序出了问题,然后疯狂点界面,直至Windows弹出强制关闭窗口……
为了使这种情况基本没有影响,只能给TinText加上线程,让Tin在子线程中被渲染。
为什么是线程而不是进程
之所以使用多线程,主要还是因为tkinter使用的tcl与Python毕竟不是同一个语言,Python只是调用tcl/tk的lib和dll库,再通过tkinter实现封装。具体原因如下:
- tcl无法提供通用的多进程,也没有官方可用的多进程库。
- Python的tcl接口中不包含多线程库(tcl-thread),也就是说tkinter的多线程是Python的多线程。
- 就算使用Python的多进程,tkinter(tcl)无法在不同的进程池中共享同一个窗口的资源,更何况tcl是被调用的。
- 如果强行使用Python的多进程,tcl无法共享变量,就会再创建(复制)一个一模一样的程序环境,在tkinter中表现为一模一样的窗口界面。
综上,因为Python使用tcl本身的限制,我们只能使用Python的Thread。
使用拓展还是自己敲
根据一篇关于tkinter是否线程安全的文章,可以得出如下结论:
tkinter模块的基础是tcl/tk,它有两种编译方式,支持线程或不支持线程。tkinter从8.6版本开始,默认的编译方式就是支持线程,即线程安全。
但是有人在一些搜索引擎上搜到过支持线程安全的tkinter拓展,比如:threadsafe-tkinter。
如果你使用在大型项目,我就不。。。。。。Tin也是大型项目……
总而言之,我们只是为了使TinEngine在使用的时候不影响主程序的正常工作,没必要大费周章地使用tkinter的多线程支持(除非你的项目本来就需要)。下面,就开始改进TinEngine自身的多线程支持。
多线程代码
在往下面看之前,默认读者知道Python多线程基础知识(基础知识足矣)。
Python的threading很简单,你可能认为这样写就对了:
#前面的界面代码省略,TinText的变量名为text
#已经 import 了 threading 库
def paint_tin(unit:list):
#unit 为要渲染的标记列表
t=threading.Thread(target=text.point_file,args=(unit,))
t.start()
if __name__=='__main__':
paint_tin(['<main>这里是Tin标记列表','<main>只要是Tin标签就行'])
然而如果真怎么写,会发现如下问题:
- <askyesno>(这个可能)和<key>标签会报错
- <pass>以及<-pass->标签中的内容,即使密码正确也不显示
其对应原因如下:
- 在Python使用多线程时,子线程的对话框没有父窗口,因为父窗口在主线程,子线程没有父窗口
- 因为子线程对话框报错,容器标签的释放前提也就始终为 False
解决思路
为了解决以上提到的问题,我们就要有清晰的改进思路。
先解决<key>标签问题,<askyesno>标签的问题同理可解。
以下是“key”(密码输入标签)原来的代码:
#...
elif tag[0]=='<key>':#密码
u=i[5:].split(';')#i 为原标签行字符串
key=u[0]
try_pass=''
if len(u)==2:
try_pass=u[1]#密码提示
answer=askstring(title='TinReader-需要权限',prompt='请输入第%d个密码以获得接下来的非公开内容:' % mima,initialvalue=try_pass,ico=TinPath+'\\Tin.ico')
if answer==key:#key 为密码
keypass=True#keypass 为释放锁,在对<pass>和<-pass->标签解析中会用到
elif answer==None:
keypass=False
else:
keypass=False
mima+=1#mima 为密码次数,方便读者知道目前密码所在的位置
#...
因为是多线程,而 answer 创建在主线程中,因此我们在将 askstring 的返回值赋予给 answer 时,我们要先定义 answer。最简单的定义就是赋值为 None。
然后是 askstring 对话框,在多线程中,askstring会在输入后返回一个错误,即主窗口在对话框创建之前就被销毁。很疑惑对不对?但很好理解,因为子线程中根本就没有主窗口,而tcl中的全局主窗口在Python的子线程中并不存在。因此,我们要在对话框创建之前建立一个主窗口,同时将对话框的父窗口指定为这个主窗口。同时,该主窗口应该是不可见的。因此下面是正确代码:
正确代码
#...
elif i[:5]=='<key>':#密码
u=i[5:].split(';')
key=u[0]
try_pass=''
if len(u)==2:
try_pass=u[1]#密码提示
answer=None#多线程必须提前定义
key_root=Tk()#在子线程中创建一个主窗口
key_root.withdraw()#不显示
#通过parent参数指定父窗口
answer=askstring(parent=key_root,title='TinReader-需要权限',prompt='请输入第%d个密码以获得接下来的非公开内容:' % mima,initialvalue=try_pass,ico=TinPath+'\\Tin.ico')
key_root.destroy()#销毁窗口,避免占内存
if answer==key:
keypass=True
elif answer==None:
keypass=False
else:
keypass=False
mima+=1
#...
这样,问题就被轻松解决了。
结语
目前,TinEngine已经基本实现多线程支持(一个TinText可以同时渲染两套甚至是多套Tin标记),已经能够胜任复杂的tkinter窗口中“TinLayout”的功能。