PyQt:在使用线程时访问对象
我现在正在做一个小型的Markdown编辑器,但遇到了一个问题:
虽然这个Markdown模块运行得很快,但它并不能魔法般地处理所有事情。因为每次文本有变化时,它都会处理整个文本,所以当我按住退格键的时候,程序会变得无响应。
我该如何使用线程(比如子进程或QThread)来实现以下功能呢?
- 在文本变化时,如果正在处理的任务还没完成,就把这个任务放到队列里。如果队列里已经有任务了,就用新的任务替换掉旧的。
- 如果文本变化时队列里没有任务,就直接执行新的任务。
- 当一个任务完成后,队列里的下一个任务就会被执行。如果队列里没有任务了,那就结束了。
说明
我并不想要上面提到的算法,而是想找到一种方法,让渲染过程既持续又不会变慢。我想在“后台”执行渲染任务,但每次只执行一个,确保总是执行最新的任务。
- 用户开始修改文本。
- 在另一个线程中启动当前文本的渲染任务。
- 一旦渲染完成,并且新的HTML替换了旧的HTML,就要判断是否还有渲染任务需要完成,也就是看看最新的文本是否已经被渲染,还是说渲染的是旧版本。
- 如果文本和渲染还不同步,就回到第2步。
注意,上面“说明”部分的算法是一种实现方式,其中第3步(判断是否不同步)是通过一个单线程队列来实现的。同时要注意,只有最新的任务是真正需要完成的,但我也希望能完成一些随机的中间任务,以防止大量新输入的内容一次性插入。
理想情况下:在一个大型文档中快速连续输入时,渲染视图每隔几个字就更新一次,而编辑器始终保持响应。
2 个回答
可以这样做:每次文本发生变化时,先检查一下渲染线程是否正在工作(用一个标记来表示)。如果没有在工作,就让它渲染一份文本编辑器里的内容。然后,渲染线程在每次渲染结束时,会把当前编辑器的内容和上一次的内容进行对比。如果发现有不同的地方,就重新渲染一次。
在我看来,这个问题适合用 QThreadPool
和 QRunnable
来解决:
- 首先,创建一个类,继承自
QRunnable
,这个类用来描述一个“渲染任务”。 - 然后,创建一个
QThreadPool
,并把maxThreadCount
设置为 1(这点非常重要,因为一次只能有一个渲染线程在工作)。 - 每当文本发生变化时,创建你刚才定义的“渲染任务”类的一个实例(最好开启
autoDelete
),然后用QThreadPool.start()
将它添加到线程池中。QThreadPool
会维护一个待处理任务的队列,所以即使当前有其他任务在运行,新任务也不会丢失。它会在当前任务完成后再执行。
为了限制创建渲染任务的数量,你不应该直接在 .textChanged
信号上启动任务,而是通过 QTimer
间接启动。每次文本变化时,重启你的定时器,设置一个大约 500 毫秒的超时,只有当定时器真正发出 .timeout
信号时,才开始一个新的“渲染任务”。
在实现“渲染任务”类时,记得不要直接访问任何 GUI 类。相反,在你的“渲染任务”类中定义一个信号,当渲染完成时发出这个信号,并把渲染好的 HTML 字符串作为参数传递,然后将这个信号连接到你显示控件的 .setText()
方法。
这并不是你所要求的完全解决方案,因为正在运行的渲染任务不会被打断,而是会一直完成。因此,如果文本变化得非常快(比如用户一直按着退格键),预览可能会出现不同步的情况。不过,我认为这是解决你问题最简单、最直接的方法,因为它不需要锁定、同步或跟踪当前正在运行的任务。每个任务都是完全异步运行的,像“发出后就不管”的风格。