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()

  1. asyncio.run 函数启动事件循环,驱动这个协程,最终也将启动其他协程。main函数保持阻塞,直到supervisor,有返回值.sueprvisor的返回值将变成asyncio.run的返回值
  2. 原生协程使用async def 定义
  3. asyncio.create_task 调度spin 最终执行,现在立即返回一个asyncio.Task 实例
  4. spinner 对象的字符串表示形式:spinner object:<Task pending name='Task-2' coro=<spin() running at E:\PyProject\pytestDemo\spinner_async.py:9>>
  5. await 关键字调用slow, 阻塞supervisor,直到slow返回
  6. 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
  1. 与 thread 版本不同,这里不需要通过Event 信号执行slow 的工作已经做完
  2. 使用asyncio.sleep(.1) 代替time.sleep(.1) 暂停时不阻塞其他协程
  3. 在控制这个协程的Task 实例上调用cancel 方法,抛出asyncio.CancelledError。捕获到这个异常就i退出循环
  4. 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()
  1. check函数 返回一个Result 元组,包含is_prime 调用返回的布尔值和用时
  2. check(n) 调用 is_prime(n),并计算用时,返回一个Result
  3. 调用check检查样本中的每个数,显示结果。
  4. 计算并显示总用时。

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)
posted @ 2024-05-26 21:29  chuangzhou  阅读(12)  评论(0编辑  收藏  举报