12月19, 2019

Python 做 UI 超 easy(6.3)——分身有术,利用多线程并行处理

上一节已经完成对服务端的改造,服务端可以正常将客户端 POST 过来的数据 print 出来,起码逻辑上服务端已经具备接收能力,虽然很糙。

TIPS
无论大产品、小产品,都是从粗糙到细致,不断 ”打磨“,最终逼近目标,不要对半成品有偏见。

有同学在 6.2 节遇到问题: 没有合适的 HTTP 调试工具,对命令行使用 Curl 也不熟悉。下午抽空写了个简单的图形化 HTTP 调试工具,纯 Python 的,大家可以按需取用。下面是使用场景。

img

继续上一节的问题,服务端收到数据,如何在视窗中输出?

一个程序,两个死循环!

按找常识来说,一个流程一旦陷入死循环,就不可能再执行其他的死循环了。

title

但是我们确实需要两个死循环分别用于:

  • 客户端可能随时发来数据,在死循环中等待来自网络的数据;
  • 一个循环来等待 window.read 函数返回。

按照常规方法,显然是二者无法兼顾了。

TIPS
有同学可能会问,为什么两个逻辑不在同一个循环里解决呢?这是个很好的想法,但是我们使用的 bottle 库没有直接暴露事件循环给我们,暂时不考虑这种做法。不过这是一个很棒的想法,工程中也确实有这种做法:多种库由统一的事件循环驱动。

如果计算机一次只能干一件事,那太无聊了,现实中我们也会一边听歌、一边码字。计算机具备同时干多件事的能力。

TIPS
实际上即使是听歌一件事,对计算机来说也是多个任务构成的:听歌不停变化的软件界面、音频解码等等。

计算机同时处理多个事务,就是它的并行计算。并行处理可选的技术有:

  • 多线程
  • 多进程

等技术,在逻辑上它们二者差异不大,差异可以百度。我们今天使用多线程技术。

TIPS
Python 编程一定要站在巨人的肩膀上,多借助第三方库。

threading 库可以帮助我们在一个程序里,运行多个业务流,实现并行处理。

利用并行处理技术,前面遇到的两个死循环的流程,看起来就会是这样:

title

图最底下的 Queue 是两个线程之间的红色连接线的放大。

左边是视窗的事件循环,与之前天气预报程序的循环是一样的;右边是 bottle 的事件循环,功能是上一节实现的 web 服务,可以接受客户端 post 的数据。它们各自所在的程序执行流,称之为线程,它们的执行进程互不干扰,各自为政。

先有沟通,才能协作

解决了一个程序执行两个事件循环的问题之后,两个线程不能各自割据,它们被创造出来是为了共同协作,实现我们要的功能:接收客户端的数据,并显示在视窗里。web 服务线程需要将收到的消息,发送给视窗线程。

多个线程之间协作,与多人协作一样,取拿之间如果协商不好,就会发生 “竞争”,比如刚取了一半新数据就覆盖了剩下的数据,这当然不是我们想看到的。一种做法是在两个线程之间放置一个管道,有点像传送带。web 服务线程一收到消息,就放到这个管道的一端,视窗线程则从另外一边不断的取出消息。

TIPS
这种管道实际上是先进先出(FIFO)队列,与平时排队类似,出入口各一;其他的还有先进后出队列,出入口共用。

我们会使用 queue 库来实现这样的功能。

视窗的同步和异步

bottle 库的事件处理本身就是异步的,当有新的消息的时候,消息处理函数才会被执行(称为回调用函数 callback function),新消息总是会被及时放到管道中。但是视窗事件循环却不是这样。

观察之前天气预报程序中 window.read() 一行,如果在其后一行设置断点,你会发现如果视窗中没有按钮被按下,Python 执行流会永远 “卡” 在 window.read(),无法执行。

img

TIPS
在第 31 行设置了断点,这一行已经处于 “事件循环” 中。
点击按钮以后,Python 才会停在第 31 行的断点处。
之后单步执行,一轮循环结束重新进入第 29 行的时候,可以看到 Python 没有继续向下执行,而是卡在第 29 行,直到 read 函数再次被按钮触发返回,才继续执行。

我们一直以来用的都是 PySimpleGUI 的默认模式——同步模式,除此之外还有异步模式,正好可以解决手头的问题。 title

同步模式是完全由用户操作驱动的,“敌不动,我不动”,如果用户不点击按钮,整个程序就死等。好比有的人比较憨,打一下动一下,不打不动。

同步模式有什么好处呢?逻辑简单!来活干活调整视窗,没活歇着,简单明了。同时带来的局限性就是,程序无法主动更新视窗,因为没有事件触发的话, 除了 read 函数,其他什么指令都不会被执行。

这种情况下,如果没有用户干预,管道里 web 服务发来的消息就永远不会被取出了,肯定是不行的。总不能让用户守着,过一会而点一下按钮:“你看看有人给我发消息没有”。这就成了大家所谓的 “智障” 产品了。我们需要视窗更主动一些:

  • 如果有用户触发,处理用户行为;
  • 如果有空(比如过了 100 毫秒用户也没有动作),就去看看有没有新消息,如果有的话,update 到视窗中。

这样程序就可以抽空( 100 毫秒没有用户事件发生 )去检查新消息。在异步模式下,有两种事件让 read 函数返回:

  • 用户触发;
  • 达到指定的超时时间,超时触发;

与用户点击按钮会将 event 设置为触发组件的 key 类似(希望你还记得,它是字符串,比如 “-CITY-”),超时触发的时候 event 也会被设置为一个 key:"__TIMEOUT__"。

TIPS
"__TIMEOUT__" 是超时事件的默认 event 名称,你也可以在 read 函数中用 timeout_key 参数指定一个新的字符串,作为超时事件的 event 名称。

我们做一个简单的 demo 演示这种超时,我们做一个计时器,每 1s 加 1,设置这么长的间隔,目的是让大家理解,PySimpleGUI 是如何抽空去更新视窗中的 “计数” 的。

代码在这里!

img

可以看到,如果用户不点击触发按钮事件,read 就会因为被超时触发,进入处理超时事件的代码:增加计数。

一旦我们快速的触发按钮事件,发现计数器不会增加了,这是因为每触发一次事件,read 函数的计数器就会重置,重新开始计数,下一轮依然有用户输入的话,超时任务就不会被执行。

实际应用中,超时事件在十几毫秒到 200 毫秒之间,可以保证在用户的两次点击之间,超时逻辑肯定会被执行几次。

只要没有用户输入,超时逻辑就会被周期性的执行,这非常适合主动更新视窗的工作,我们稍后的工作也是围绕着超时事件的处理来做,完成从管道中取数据,更新显示的工作。

俯瞰全局

前面探讨的内容,主要解决了几个问题:

  • 怎么在一个程序里跑多个死循环:用多线程;
  • 多个线程怎么协作:用先进先出队列当作通信管道;
  • 视窗线程无法主动更新显示:使用 read 函数的异步模式,产生超时事件来主动更新显示。

让我们将上一节中的这个图: title

结合今天得到的三个新解决方案,得到这样的图示: title

我们会在 web 服务线程的 post 方法请求函数中将收到的消息放入管道;视窗线程定时检查管道,有消息的话,就更新显示。

其实逻辑不是很复杂,这一图足以,除了我的艺术细菌有点变异之外,笔芯~

总结:

本来今天想一鼓作气把代码讲完,但是一不小心已经这么长的内容了,不如放到明天,从容的展开好了。

编程其实只要能过了语言关,剩下的主要就是:

  • 逻辑问题、
  • 第三方库、

问题,假以时日打通任督二脉,什么语言就都不在话下。

编程的核心在于思想,就像写作的精髓在故事,而不是写字本身。

加油了,明天见~

本文链接:http://www.thinkinpython.com/post/PySimpleGUI6_3.html

-- EOF --