系统学习Python——并发模型和异步编程:基础实例-[使用线程实现旋转指针]
分类目录:《系统学习Python》总目录
在讨论线程以及如何避免GIL的过程中,Python贡献者Michele Simionato发布了一个示例,可以看作演示并发的“Hello World”示例,即能展示Python“一心二用”最简单的程序。Simionato的程序使用的是multiprocessing
,经过我们修改,又分别实现了使用threading
和asyncio
的版本。
接下来的示例的想法很简单:启动一个函数,阻塞3秒,期间在终端展示字符动画,让用户知道程序正在运转,没有停滞。这个脚本在界面上的相同位置依次显示字符串\|/-
中的各个字符,实现旋转指针动画。当缓慢的计算结束后,旋转指针那一行内容清空,显示结果:Answer: 42
。下面的代码是线程threading
版本:
import itertools
import time
from threading import Thread, Eventdef spin(msg: str, done: Event) -> None: # 这个函数将在单独的线程中运行。done参数的值是一个threading.Event实例,一个用于同步线程的简单对象。for char in itertools.cycle(r'\|/-'): # 这是一个无限循环,因为itertools.cycle一次产出一个字符,一直反复迭代字符串。status = f'\r{char} {msg}' # 用文本实现动画的技巧:使用ASCII回车符('\r')把光标移到行头。print(status, end='', flush=True)if done.wait(.1): # 如果其他线程设置了这个事件,则Event.wait(timeout=None)方法返回True;经过timeout指定的时间后,返回False。这里把暂停时间设为0.1秒,作用是把动画的帧率设为10fps。如果我们希望指针旋转快一些,那就把暂停时间值设置小一些。break # 退出无限循环。blanks = ' ' * len(status)print(f'\r{blanks}\r', end='') # 退出无限循环。def slow() -> int:time.sleep(3) # slow()由主线程调用。假设有一个API调用通过网络发送,速度很慢。调用sleep阻塞主线程,但是GIL已被释放,因此指针还能继续旋转。return 42def supervisor() -> int: # supervisor返回slow的结果.done = Event() # threading.Event实例是协调main线程和spinner线程活动的关键,详见下面的分析spinner = Thread(target=spin, args=('thinking!', done)) # 创建一个Thread实例,target关键字参数的值是一个函数,args参数的值是一个元组,即传给target函数的位置参数。print(f'spinner object: {spinner}') # 显示spinner对象。输出是<Thread(Thread-1,initial)>,其中initial是线程的状态,表示尚未启动。spinner.start() # 启动spinner线程。result = slow() # 调用slow,阻塞main线程。同时,次线程运行旋转指针动画。done.set() # 把Event标志设为True,终止spin函数中的for循环。spinner.join() # 等待,直到spinner线程结束。return resultdef main() -> None:result = supervisor() # 运行supervisor函数。之所以分别定义main和supervisor两个函数,是为了与后续中的asyncio版本保持对应。print(f'Answer: {result}')if __name__ == '__main__':main()
通过上面的示例,我们了解到的最重要的一点是,调用time.sleep()
阻塞所在的线程,但是释放GIL,其他Python线程可以继续运行。
spin
和slow
两个函数并发执行。主线程(启动程序时唯一的线程)将启动一个新线程运行spin
,然后调用slow
。Python没有提供终止线程的API,如果想终止线程,则必须向线程发送相应的消息。在Python中,协调线程的信号机制,使用threading.Event
类最简单。Event
实例有一个内部布尔标志,开始时为False
。调用Event.set()
可把这个标志设为True
。这个标志为False
时,在一个线程中调用Event.wait()
,该线程将被阻塞,直到另一个线程调用Event.set()
,致使Event.wait()
返回True
。使用Event.wait(s)
设置一个暂停时间(单位为秒),经过这段时间后,Event.wait(s)
调用返回False
,如果另一个线程调用Event. set()
,则立即返回True
。下面的示例中的supervisor
函数使用一个Event
实例向spin
函数发送退出信号。
main
线程设置done
事件后,spinner
线程终将收到信号,干净退出。
参考文献:
[1] Mark Lutz. Python学习手册[M]. 机械工业出版社, 2018.
[2] 卢西亚诺·拉马略.流畅的Python 第2版(全2册) 编程语言[M].人民邮电出版社,2023.