用Python创造一门标记语言并渲染(5)——线程

  • 引言
  • 为什么是线程而不是进程
  • 使用拓展还是自己敲
  • 多线程代码
  • 解决思路
  • 正确代码
  • 结语

引言

经过前面几篇文章,一个标记语言(Tin)的基本框架、渲染的基本思路已经阐述完毕,Tin已经能够被正常地解析渲染了。那么接下来我们就应该开开心心地使用TinEngine做我们想做的事(事实上一直到TinEngine-2.2.0-之前我都没考虑),但总有那么不顺心的情况发生。

当使用耗时标签时(特别是“stop”标签),整个程序界面就会被卡住,当延时暂停时间达到10s(不一定是这个时间)以上时,界面标题会在后面显示“(未响应)”,读者(用户)还以为是不是程序出了问题,然后疯狂点界面,直至Windows弹出强制关闭窗口……

为了使这种情况基本没有影响,只能给TinText加上线程,让Tin在子线程中被渲染。

为什么是线程而不是进程

之所以使用多线程,主要还是因为tkinter使用的tcl与Python毕竟不是同一个语言,Python只是调用tcl/tk的lib和dll库,再通过tkinter实现封装。具体原因如下:

  1. tcl无法提供通用的多进程,也没有官方可用的多进程库。
  2. Python的tcl接口中不包含多线程库(tcl-thread),也就是说tkinter的多线程是Python的多线程。
  3. 就算使用Python的多进程,tkinter(tcl)无法在不同的进程池中共享同一个窗口的资源,更何况tcl是被调用的。
  4. 如果强行使用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标签就行'])

然而如果真怎么写,会发现如下问题:

  1. <askyesno>(这个可能)和<key>标签会报错
  2. <pass>以及<-pass->标签中的内容,即使密码正确也不显示

其对应原因如下:

  1. 在Python使用多线程时,子线程的对话框没有父窗口,因为父窗口在主线程,子线程没有父窗口
  2. 因为子线程对话框报错,容器标签的释放前提也就始终为 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”的功能。