Python - 并发模型
使用线程实现旋转指针
import itertools
import time
from threading import Thread, Event
def spin(msg: str, done: Event) -> None:
for char in itertools.cycle(r'\|/-'): # 1
status = f'\r{char}{msg}' # 2
print(status, end='', flush=True)
if done.wait(.1): # 3
break # 4
blanks = ' ' * len(status)
print(f'\r{blanks}\r', end='') #5
def slow() -> int:
time.sleep(3) # 7
return 42
'''
1. 这是一个无限循环,因为itertools.cycle 一次产出一个字符,一直反复迭代字符串
2. 用文本实现动画的技巧: 使用ASCCI 回车符('\r') 把光标移动到行头
3. 如果其他线程设置了set(),则Event.wait(timeout=None) 方法返回True
如果其他线程未设置set(), 则经过timeout 时间后返回False,这里把暂停时间设置为0.1秒,作用是把动画的
帧率设为10fps。如果想指针旋转的快些,可以把值设置的小一些
4. 退出无限循环
5. 显示空格,并把光标移到开头,清空状态行
6.slow() 由主线程调用。假设有一个API通过网络发送,速度很慢。调用sleep 阻塞主线程,但是GIL已被释放,因此指针还能继续转动
'''
def supervisor() -> int: # 1
done = Event() # 2
spinner = Thread(target=spin, args=('thinking!', done)) # 3
print(f'spinner object:{spinner}') # 4
spinner.start() # 5
result = slow() # 6
done.set() # 7
spinner.join() # 8
return result
def main() -> None:
result = supervisor()
print(f'AnswerL: {result}')
'''
1.supervisor 反回slow结果
2.threading.Event 实例是协调 main线程和spinnner 线程活动的关键
3.创建一个Thread 实例,target 的值是一个函数,args 参数的值是一个元组,即传给target函数的参数
4.显示spinner对象。输出是spinner object:<Thread(Thread-1 (spin), initial)>,其中initial是线程的动态,表示尚未启动
5.启动线程
6.调用slow,阻塞main线程。同时,次线程运行旋转动画指针
7.把Event标志设置为True,终止spin函数中的for循环
8.等待,直到spinner 线程结束
'''
if __name__ == '__main__':
main()
运行中:
运行结束:
通过这个实例,要了解最重要的一点:
调用 time.sleep()阻塞所在的线程,但是释放GIL,其他Pyhton线程可以继续运行
在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
使用进程实现旋转指针
import itertools
import time
from multiprocessing import Process, Event # 1
from multiprocessing import synchronize # 2
def spin(msg: str, done: synchronize.Event) -> None: # 3
for char in itertools.cycle(r'\|/-'):
status = f'\r{char}{msg}'
print(status, end='', flush=True)
if done.wait(.1):
break
blanks = ' ' * len(status)
print(f'\r{blanks}\r', end='')
def slow() -> int:
time.sleep(3)
return 42
def supervisor() -> int:
done = Event()
spinner = Process(target=spin, args=('thinking!', done)) # 4
print(f'spinner object:{spinner}') # 5
spinner.start()
result = slow()
done.set()
spinner.join()
return result
def main() -> None:
result = supervisor()
print(f'AnswerL: {result}')
if __name__ == '__main__':
main()
'''
1.multiprocessing API 基本模仿threading API,不过类型提示和Mypy还是揭示了一处区别:
multiprocessing.Event 是函数(threading.Event 是类),返回synchronize.Event实例
2.因此需要编写这个类型提示
3.才能编写这个类型提示
4.Process 类的基本用法与Thread 相似
5.spinner 对象显示为spinner object:<Process name='Process-1' parent=4076 initial>,其中4076 是运行spinner_proc.py 的Python实例的进程ID
'''
threading和multiprocessing 的API基本相同,但是实现方式差别很大,而且为了处理多进程编程增加的复杂度,multiprocssing 的API更多。例如,把线程换成进程后,一个难点是如何在被操作系统隔离且无法共享Python对象的进程之间通信。为此,对跨进程传递的对象需要序列化和反序列化,这样一来开销就增加了,在该实例中,跨进行传递的数据只有Event状态。在multiprocessing 模块底层的C代码中,Event状态通过操作系统底层信号量实现
使用协程实现旋转指针
线程和进行由操作系统调度程序分配CPU时间驱动。相比之下,协程由应用级事件循环驱动:事件循环管理待定协程队列,逐个驱动,监视由协程发起的I/O操作触发的事件,在各个事件发生时把控制权传回相应的协程。事件循环、库协程以及用户协程都在同一个线程中执行。因此,在协程中花费的任何时间都会被减慢事件循环,以及所有其他协程。
从main函数入手,再分析supervisor 函数,更易于理解旋转指针程序的协程版本。详见示例19-4
# 示例19-4 spinner_async.py :main 函数和supervisor 协程
def main() -> None:
result = asyncio.run(superviosr()) # 1
print(f'Answer:{result}')
async def superviosr() -> int: # 2
spinner = asyncio.create_task(spin('thinking!')) # 3
print(f'spinner object:{spinner}') # 4
result = await slow() # 5
spinner.cancel() # 6
return result
if __name__ == '__main__':
main()
- asyncio.run 函数启动事件循环,驱动这个协程,最终也将启动其他协程。main函数保持阻塞,直到supervisor,有返回值.sueprvisor的返回值将变成asyncio.run的返回值
- 原生协程使用async def 定义
- asyncio.create_task 调度spin 最终执行,现在立即返回一个asyncio.Task 实例
- spinner 对象的字符串表示形式:spinner object:<Task pending name='Task-2' coro=<spin() running at E:\PyProject\pytestDemo\spinner_async.py:9>>
- await 关键字调用slow, 阻塞supervisor,直到slow返回
- Task.candel 方法在spin协程中抛出CancelError异常。
-
asyncio.run(): 在常规函数中调用,驱动一个协程对象,通常作为程序中所有异步代码的入口,例如本例中的supervisor。这个调用保持阻塞,一直到coro 的主体返回。run 调用的返回值是 coro 主体的返回值
-
asyncio.create_task(coro()): 在协程中调用,调度另一个协程最终执行。这个调用不中止当前协程。返回一个Task实例。包装协程对象,提供控制和查询协程状态的方法
-
await coro():在协程只调用,把控制权转给coro() 返回的协程对象。中止当前协程,知道coro 的主体返回。这个异步等待表达式的值是coro 主体的返回值
记住,通过coro() 调用协程立即返回一个协程对象,但是不运行coro 函数的主体。协程的主体由事件循环驱动
下面分析 19-5 中的spin 和 slow 协程
# 示例19-5 spinner_async.py: spin 和 slow 协程
import asyncio
import itertools
async def spin(msg:str) -> None: # 1
for char in itertools.cycle(r'\|/-'):
status = f'\r{char}{msg}'
print(status,flush=True,en d='')
try:
await asyncio.sleep(.1) # 2
except asyncio.CancelledError: # 3
break
blanks = ' ' * len(status)
print(f'\r{blanks}\r', end='')
async def slow() -> int:
await asyncio.sleep(3) # 4
return 42
- 与 thread 版本不同,这里不需要通过Event 信号执行slow 的工作已经做完
- 使用asyncio.sleep(.1) 代替time.sleep(.1) 暂停时不阻塞其他协程
- 在控制这个协程的Task 实例上调用cancel 方法,抛出asyncio.CancelledError。捕获到这个异常就i退出循环
- slow协程也使用 await asyncio.sleep 代替tiem.sleep
实验:故意破环,深入理解
更改程序:
async def slow() -> int:
# await asyncio.sleep(3)
time.sleep(3)
return 42
实验现象:
- 旋转指针一直没有出现,程序挂起3秒
- 显示Answer: 42,程序终止
理解以上行为的关键要知道,使用asyncio 的Python代码只有一个执行流,除非显示启动额外的线程或进程。这意味着 任何一个时间点上只有一个协程在执行。若想实现并发,则要那控制权由一个协程传递给另一个协程。
time.sleep 版本的superviosr:
async def superviosr() -> int:
spinner = asyncio.create_task(spin('thinking!'))
print(f'spinner object:{spinner}')
result = await slow() # 1
spinner.cancel() # 2
return result
'''
1.time.sleep(3) 阻塞3秒,程序什么也做不了。因为主线程被阻塞了,而主线程是唯一的线程。操作系统继续其他活动。3秒
过后sleep 不再阻塞,slow 返回
2.slow 一旦返回,spinner 任务就被取消。控制流始终没有处级spin协程的主体
3.在asyncio 创建的协程中千万不要使用time.sleep。除非想暂停整个程序。如果希望协程空闲一段时间,什么也不做,那么
应该使用await asyncio.sleep(DELAY)。把控制权交给事件循环,驱动其他待定协程。
'''
自建进程池
# 示例19-12 sequential.py:对一个小型数据集做指数检测(顺序执行版)
"""
sequential.py: CPU密集型工作的顺序执行版,多线程版和多进程版的比较基准
"""
from time import perf_counter
from typing import NamedTuple
from primes import is_prime, NUMBERS
class Result(NamedTuple): # 1
prime: bool
elapsed: float
def check(n: int) -> Result: # 2
t0 = perf_counter()
prime = is_prime(n)
return Result(prime, perf_counter() - t0)
def main() -> None:
print(f'Checking {len(NUMBERS)} numbers sequentially:')
t0 = perf_counter()
for n in NUMBERS: # 3
prime, elapsed = check(n)
label = 'P' if prime else ' '
print(f'{n:16} {label} {elapsed:9.6f}s')
elapsed = perf_counter() - t0 # 4
print(f'Total time:{elapsed:.2f}s')
if __name__ == '__main__':
main()
- check函数 返回一个Result 元组,包含is_prime 调用返回的布尔值和用时
- check(n) 调用 is_prime(n),并计算用时,返回一个Result
- 调用check检查样本中的每个数,显示结果。
- 计算并显示总用时。
result:
Checking 20 numbers sequentially:
2 P 0.000000s
142702110479723 P 0.292459s
299593572317531 P 0.394948s
3333333333333301 P 1.327670s
3333333333333333 0.000007s
3333335652092209 1.346595s
4444444444444423 P 1.577922s
4444444444444444 0.000001s
4444444488888889 1.563284s
5555553133149889 1.731576s
5555555555555503 P 1.741148s
5555555555555555 0.000006s
6666666666666666 0.000000s
6666666666666719 P 1.926269s
6666667141414921 1.916402s
7777777536340681 2.079374s
7777777777777753 P 2.086385s
7777777777777777 0.000006s
9999999999999917 P 2.374654s
9999999999999999 0.000006s
Total time:20.36s
上面案例所依赖的primes.py:
```python
# primes.py
import math
PRIME_FIXTURE = [
(2, True),
(142702110479723, True),
(299593572317531, True),
(3333333333333301, True),
(3333333333333333, False),
(3333335652092209, False),
(4444444444444423, True),
(4444444444444444, False),
(4444444488888889, False),
(5555553133149889, False),
(5555555555555503, True),
(5555555555555555, False),
(6666666666666666, False),
(6666666666666719, True),
(6666667141414921, False),
(7777777536340681, False),
(7777777777777753, True),
(7777777777777777, False),
(9999999999999917, True),
(9999999999999999, False),
]
NUMBERS = [n for n, _ in PRIME_FIXTURE]
# tag::IS_PRIME[]
def is_prime(n: int) -> bool:
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
root = math.isqrt(n)
for i in range(3, root + 1, 2):
if n % i == 0:
return False
return True
if __name__ == '__main__':
for n, prime in PRIME_FIXTURE:
prime_res = is_prime(n)
assert prime_res == prime
print(n, prime)
本文来自博客园,作者:chuangzhou,转载请注明原文链接:https://www.cnblogs.com/czzz/p/18214323