Python 协程并发
date
Mar 24, 2020
slug
5589752779597824
status
Published
tags
Python
summary
type
Post
先行内容
同步和异步
同步和异步关注的发送方和接收方是否协调步调一致。
同步:发送方发出请求后,等接收方发回响应以后才发下一个请求。
异步:发送方发出请求后,不等接收方发回响应,接着发送下个请求。
同步是指一个线程要等待上一个线程执行完之后才开始执行当前的线程。
异步是指一个线程开始执行,它的下一个线程不必等待它执行完成就可以开始执行。
阻塞和非阻塞
阻塞和非阻塞关注的是程序在等待调用结果时的状态。
当一个进程或线程被执行,他的下一个进程或线程需要等待它执行完成才能被执行,此时他的下一个进程或线程处于阻塞态。
阻塞和同步是描述一致的,只是关注点不同,同步是关注发送方和接收方的传递,阻塞是关注发送方和接收方传递的过程
我们去火车站人工购票时需要排序购票,当我们前面有其他人在进行排队或购票时我们要等待,此时可以说我们是处于阻塞态,这种购票方式可以认为是同步。
如果是网络购票,我们无需考虑前面是否有人,直接在购票即可,这是一种异步行为,这个过程是非阻塞状态。
并行和并发
并行:两个或者多个事件在同一时刻发生。
并发:两个或多个事件在同一时间间隔内发生。
通俗的说并行是指两个或多个任务同时执行,多线程是并行的;并发是两个或多个任务进行交替执行,某一时间段内只有一个任务在执行,协程是并发的。
协作式多任务和抢占式多任务
协程是协作式多任务的,而线程典型是抢占式多任务的
线程
线程(thread)是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
👉Python 标准库之 threading (线程并行)
正文
协程
协程(coroutine)是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复。
协程通过
async/await
语法进行声明声明一个协程函数:
async def f1(): pass
运行协程:
直接调用不会执行协程,标准库 asyncio 提供了相关功能
asyncio 有三种方式执行协程
- 通过
asyncio.run()
运行一个作为入口的协程函数
import asyncio async def f1(): print("hello") asyncio.run(f1())
- 在线程函数种通过
await
运行一个协程函数
import asyncio async def f2(): print("f2") async def f1(): print("f1") await f2() asyncio.run(f1())
运行结果:
f1 f2
- 通过
asyncio.create_task()
创建协程任务后使用await
执行
import asyncio async def f2(): print("f2") async def f1(): print("f1") task = asyncio.create_task(f2()) await task # f1()asyncio.run(f1())
运行结果:
f1 f2
可等待对象
如果一个对象可以在
await
语句中使用,那么它就是可等待对象。可等待 对象有三种主要类型: 协程, 任务(Task) 和 Future。
任务就是使用
asyncio.create_task()
创建的对象;Future是一种特殊的低层级可等待对象,一个Future代表一个异步运算的最终结果。参考传送门
import asyncio async def f2(a): a.set_result("f2") async def f1(): loop = asyncio.get_running_loop() print(type(loop)) fut = loop.create_future() print(type(fut)) loop.create_task(f2(fut)) print(await fut) asyncio.run(f1())
输出结果:
<class 'asyncio.windows_events._WindowsSelectorEventLoop'> <class '_asyncio.Future'> f2
一个协程并发执行的例子
import asyncio async def f2(a): for i in range(3): print(a+":", i) await asyncio.sleep(1) async def f1(): tasks = asyncio.gather(f2("A"), f2("B")) await tasks print("done") asyncio.run(f1())
输出结果:
A: 0 B: 0 A: 1 B: 1 A: 2 B: 2 done
协程的逻辑
- A 函数执行时被中断,传递一些数据给 B 函数;
- B 函数接收到这些数据后开始执行,执行一段时间后,返回一些数据到 A 函数;
- 交替执行,知道任务完成;
通过生成器理解协程:
def f1(): for i in range(3): from_f2 = yield i print("来自f2的值:",from_f2, i) def f2(a): from_f1 = a.send(None) print('来自f1的值:', from_f1) list = ['A', 'B', 'C'] try: for i in list: from_a = a.send(i) print('来自f1的值:', from_a) except StopIteration: print('done') f = f1() f2(f)
输出结果:
来自f1的值: 0 来自f2的值: A 0 来自f1的值: 1 来自f2的值: B 1 来自f1的值: 2 来自f2的值: C 2 done
扩展内容
生成器
引用自-维基百科生成器,也叫作“半协程”,是协程的子集。尽管二者都可以yield多次,挂起(suspend)自身的执行,并允许在多个入口点重新进入,但它们特别差异在于,协程有能力控制在它让位之后哪个协程立即接续它来执行,而生成器不能,它只能把控制权转交给调用生成器的调用者[9]。在生成器中的yield语句不指定要跳转到的协程,而是向父例程传递返回值。尽管如此,仍可以在生成器设施之上实现协程,这需要通过顶层的派遣器(dispatcher)例程(实质上是trampoline)的援助,它显式的把控制权传递给由生成器传回的令牌(token)所标识出的子生成器。在不同作者和语言之间,术语“生成器”和“迭代器”的用法有着微妙的差异。有人说所有生成器都是迭代器,生成器看起来像函数而表现得像迭代器。在Python中,生成器是迭代器构造子:它是返回迭代器的函数。
参考
asyncio 是用来编写 并发 代码的库,使用 async/await 语法。
asyncio.``run(coro, *, debug=False)
: 运行协程asyncio.create_task(coro, *, name=None)
: 创建任务coroutine asyncio.sleep(delay, result=None, *, loop=None)
: 休眠asyncio.gather(*aws, loop=None, return_exceptions=False)
: 并发运行任务asyncio.shield(aw, *, loop=None)
: 保护一个可等待对象防止其被取消asyncio.wait_for(aw, timeout, *, loop=None)
: 可等待对象完成后延时一段时间asyncio.wait(aws, *, loop=None, timeout=None, return_when=ALL_COMPLETED)
: 按条件阻塞asyncio.as_completed(aws, *, loop=None, timeout=None)
: 并发地运行 aws 集合中的 可等待对象asyncio.run_coroutine_threadsafe(coro, loop)
: 向指定事件循环提交一个协程asyncio.current_task(loop=None)
:返回当前运行的任务asyncio.all_tasks(loop=None)
: 返回事件循环所运行的未完成的任务集合asyncio.Task(coro, *, loop=None, name=None)
:@asyncio.coroutine
: 用来标记基于生成器的协程的装饰器asyncio.iscoroutine(obj)
: 如果 obj 是一个 协程对象则返回 Trueasyncio.iscoroutinefunction(func)
: 如果 func 是一个协程函数则返回 True。