概述

由于 cpu和 磁盘读写的 效率有很大的差距,往往cpu执行代码,然后遇到需要从磁盘中读写文件的操作,此时主线程会停止运行,等待IO操作完成后再继续进行,这要就导致cpu的利用率非常的低。

协程可以实现单线程同时执行多个任务,但是需要自己手动的通过send函数和yield关键字配合来传递消息,asyncio模块能够自动帮我们传递消息。

python中协程主要经历了如下三个阶段

1)生成器变形 yield/send

2)asyncio.coroutine和yield from

3)async/await关键字

## 生成器变形 yield/send

yield

Python中函数如果把return换成了yield,那么这个函数就不再普通函数了,而是一个生成器

简单生成器示例:

  1. def mygen(alist): # define a generator
  2. while alist:
  3. c = alist.pop()
  4. yield c
  5. lst = [1, 2, 3]
  6. g = mygen(lst) # get a generator object
  7. print(g) # <generator object mygen at 0x0000020225555F10>
  8. while True:
  9. try:
  10. print(next(g)) # 3 2 1
  11. except StopIteration:
  12. break

生成器本质上也是迭代器,因此不仅可以使用next()取值,还可以使用for循环取值

  1. for item in g:
  2. print(item) # 3 2 1

send

生成器函数最大的特点是可以接收一个外部传入的变量,并根据变量内容计算结果后返回,这个特点是根据send()函数实现的

send()函数使用示例:

  1. def gen():
  2. value = 0
  3. while True:
  4. receive = yield value
  5. if receive == "Q" or receive == "q":
  6. break
  7. value = "got:%s" % receive
  8. g = gen()
  9. print(g.send(None)) # 第一个必须是None,否则会报错
  10. print(g.send("hello~"))
  11. print(g.send(123))
  12. print(g.send([1, 2, 3]))

执行结果

  1. 0
  2. got:hello~
  3. got:123
  4. got:[1, 2, 3]

注意:第一个send()里传入的变量必须是None,否则会报错TypeError: can't send non-None value to a just-started generator

这里最关键的一步就是receive = yield value,这一句实际上分为三步

1)向函数外抛出(返回)value

2)暂停,等待next()或send()恢复

3)将等号右边的表达式的值(这个值是传入的)赋值给receive

下面来梳理一下执行流程

1)通过g.send(None)或者next(g)启动生成器函数,并执行到第一个yield的位置

2)执行yield value,程序返回value,也就是0,之后暂停,等待下一个next()或send(),注意这时并没有给receive赋值

3)gen返回value之后跳出,执行主程序里面的g.send("hello"),执行这一句会传入"hello",从之前暂停的位置继续执行,也就是赋值给receive,继续往下执行,value变成"got:hello~",然后判断while,执行到yield value,返回value,所以打印出"got:hello~",之后进入暂停,等待下一个send()激活

4)后续的g.send(123)执行流程类似,如果传入"q",gen会执行到break,整个函数执行完毕,会得StopIteration

从上面可以看出,在第一次send(None)启动生成器(执行1>2,通常第一次返回的值并没有什么用)之后,对于外部的每一次send(),生成器的实际在循环中的运行顺序是3–>1–>2,也就是先获取值,然后do something,然后返回一个值,再暂停等待。


### yield from

yield from是Python3.3引入的,先来看一段代码

  1. def gen1():
  2. yield range(5)
  3. def gen2():
  4. yield from range(5)
  5. iter1 = gen1()
  6. iter2 = gen2()
  7. for item in iter1:
  8. print(item)
  9. for item in iter2:
  10. print(item)

执行结果

  1. range(0, 5)
  2. 0
  3. 1
  4. 2
  5. 3
  6. 4

从上面的示例可以看出来yield是将range这个可迭代对象直接返回,而yield from解析range对象,将其中每一个item返回,yield from本质上等于

  1. for item in iterable:
  2. yield item

注意yield from后面只能接**可迭代对象**


下面来看一个例子,我们编写一个斐波那契数列函数

  1. def fab(max):
  2. n, a, b = 0, 0, 1
  3. while n < max:
  4. yield b
  5. a, b = b, a+b
  6. n += 1
  7. f = fab(5)

fab不是一个普通函数,而是一个生成器。因此fab(5)并没有执行函数,而是返回一个生成器对象,假设要在fab()的基础上实现一个函数,调用起始都要记录日志

  1. def wrapper(func_iter):
  2. print("start")
  3. for item in func_iter:
  4. yield item
  5. print("end")
  6. wrap = wrapper(fab(5))
  7. for i in wrap:
  8. print(i)

下面使用yield from代替for循环

  1. def wrapper(func_iter):
  2. print("start")
  3. yield from func_iter
  4. print("end")
  5. wrap = wrapper(fab(5))
  6. for i in wrap:
  7. print(i)

asyncio.coroutine和yield from

yield from在asyncio模块(python3.4引入)中得以发扬光大。之前都是手动的通过send函数和yield关键字配合来传递消息,现在当声明函数为协程后,我们通过事件循环来调度协程

  1. import asyncio, random
  2. @asyncio.coroutine # 将一个generator定义为coroutine
  3. def smart_fib(n):
  4. i, a, b = 0, 0, 1
  5. while i < n:
  6. sleep_time = random.uniform(0, 0.2)
  7. yield from asyncio.sleep(sleep_time) # 通常yield from后都是接的耗时操作
  8. print("smart take %s secs to get %s" % (sleep_time, b))
  9. a, b = b, a+b
  10. i += 1
  11. @asyncio.coroutine
  12. def stupid_fib(n):
  13. i, a, b = 0, 0, 1
  14. while i < n:
  15. sleep_time = random.uniform(0, 0.5)
  16. yield from asyncio.sleep(sleep_time)
  17. print("stupid take %s secs to get %s" % (sleep_time, b))
  18. a, b = b, a+b
  19. i += 1
  20. if __name__ == '__main__':
  21. loop = asyncio.get_event_loop() # 获取事件循环的引用
  22. tasks = [ # 创建任务列表
  23. smart_fib(10),
  24. stupid_fib(10),
  25. ]
  26. loop.run_until_complete(asyncio.wait(tasks)) # wait会分别把各个协程包装进一个Task 对象。
  27. print("All fib finished")
  28. loop.close()

yield from语法可以让我们方便地调用另一个generator。 本例中yield from后面接的asyncio.sleep()也是一个coroutine(里面也用了yield from),所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,线程就可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。

asyncio是一个基于事件循环的实现异步I/O的模块。通过yield from,我们可以将协程asyncio.sleep的控制权交给事件循环,然后挂起当前协程;之后,由事件循环决定何时唤醒asyncio.sleep,接着向后执行代码。

协程之间的调度都是由事件循环决定。

yield from asyncio.sleep(sleep_secs) 这里不能用time.sleep(1)因为time.sleep()返回的是None,它不是iterable,还记得前面说的yield from后面必须跟iterable对象(可以是生成器,迭代器)。


另一个示例

  1. import asyncio
  2. @asyncio.coroutine
  3. def wget(host):
  4. print('wget %s...' % host)
  5. connect = asyncio.open_connection(host, 80) # 与要获取数据的网页建立连接
  6. # 连接中包含一个 reader和writer
  7. reader, writer = yield from connect # 通过writer向服务器发送请求,通过reader读取服务器repnse回来的请求
  8. header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host # 组装请求头信息
  9. writer.write(header.encode('utf-8')) # 需要对请求头信息进行编码
  10. yield from writer.drain() # 由于writer中有缓冲区,如果缓冲区没满不且drain的话数据不会发送出去
  11. while True:
  12. line = yield from reader.readline() # 返回的数据放在了reader中,通过readline一行一行地读取数据
  13. if line == b'\r\n': # 因为readline实际上已经把\r\n转换成换行了,而此时又出现\r\n说明以前有连续两组\r\n
  14. break # 即\r\n\r\n,所以下面就是response body了
  15. print('%s header > %s' % (host, line.decode('utf-8').rstrip()))
  16. # Ignore the body, close the socket
  17. writer.close()
  18. # reader.close() AttributeError: 'StreamReader' object has no attribute 'close'
  19. if __name__ == '__main__':
  20. loop = asyncio.get_event_loop()
  21. tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
  22. loop.run_until_complete(asyncio.wait(tasks))
  23. loop.close()

## async和await

弄清楚了asyncio.coroutine和yield from之后,在Python3.5中引入的async和await就不难理解了,我们使用的时候只需要把@asyncio.coroutine换成async,把yield from换成await就可以了。当然,从Python设计的角度来说,async/await让协程表面上独立于生成器而存在,将细节都隐藏于asyncio模块之下,语法更清晰明了。

加入新的关键字 async ,可以将任何一个普通函数变成协程

一个简单的示例

  1. import time, asyncio, random
  2. async def mygen(alist):
  3. while alist:
  4. c = alist.pop()
  5. print(c)
  6. lst = [1, 2, 3]
  7. g = mygen(lst)
  8. print(g)

执行结果

  1. <coroutine object mygen at 0x00000267723FB3B8> # 协程对象
  2. sys:1: RuntimeWarning: coroutine 'mygen' was never awaited

可以看到,我们在前面加上async,该函数就变成了一个协程,但是**async对生成器是无效的**

  1. async def mygen(alist):
  2. while alist:
  3. c = alist.pop()
  4. yield c
  5. lst = [1, 2, 3]
  6. g = mygen(lst)
  7. print(g)

执行结果

  1. <async_generator object mygen at 0x000001540EF505F8> # 并不是协程对象

所以正常的协程是这样的

  1. import time, asyncio, random
  2. async def mygen(alist):
  3. while alist:
  4. c = alist.pop()
  5. print(c)
  6. await asyncio.sleep(1)
  7. lst1 = [1, 2, 3]
  8. lst2 = ["a", "b", "c"]
  9. g1 = mygen(lst1)
  10. g2 = mygen(lst2)

要运行协程,要用事件循环
在上面的代码下面加上:

  1. if __name__ == '__main__':
  2. loop = asyncio.get_event_loop()
  3. tasks = [
  4. c1,
  5. c2
  6. ]
  7. loop.run_until_complete(asyncio.wait(tasks))
  8. print("all finished")
  9. loop.close()

参考:

1)https://blog.csdn.net/soonfly/article/details/78361819

2)https://blog.csdn.net/weixin_40247263/article/details/82728437

深入理解python协程的更多相关文章

  1. 理解Python协程:从yield/send到yield from再到async/await

    Python中的协程大概经历了如下三个阶段:1. 最初的生成器变形yield/send2. 引入@asyncio.coroutine和yield from3. 在最近的Python3.5版本中引入as ...

  2. Python 协程总结

    Python 协程总结 理解 协程,又称为微线程,看上去像是子程序,但是它和子程序又不太一样,它在执行的过程中,可以在中断当前的子程序后去执行别的子程序,再返回来执行之前的子程序,但是它的相关信息还是 ...

  3. day-5 python协程与I/O编程深入浅出

    基于python编程语言环境,重新学习了一遍操作系统IO编程基本知识,同时也学习了什么是协程,通过实际编程,了解进程+协程的优势. 一.python协程编程实现 1.  什么是协程(以下内容来自维基百 ...

  4. 用yield实现python协程

    刚刚介绍了pythonyield关键字,趁热打铁,现在来了解一下yield实现协程. 引用官方的说法: 与线程相比,协程更轻量.一个python线程大概占用8M内存,而一个协程只占用1KB不到内存.协 ...

  5. [转载] Python协程从零开始到放弃

    Python协程从零开始到放弃 Web安全 作者:美丽联合安全MLSRC   2017-10-09  3,973   Author: lightless@Meili-inc Date: 2017100 ...

  6. 00.用 yield 实现 Python 协程

    来源:Python与数据分析 链接: https://mp.weixin.qq.com/s/GrU6C-x4K0WBNPYNJBCrMw 什么是协程 引用官方的说法: 协程是一种用户态的轻量级线程,协 ...

  7. python协程详解

    目录 python协程详解 一.什么是协程 二.了解协程的过程 1.yield工作原理 2.预激协程的装饰器 3.终止协程和异常处理 4.让协程返回值 5.yield from的使用 6.yield ...

  8. Python协程与Go协程的区别二

    写在前面 世界是复杂的,每一种思想都是为了解决某些现实问题而简化成的模型,想解决就得先面对,面对就需要选择角度,角度决定了模型的质量, 喜欢此UP主汤质看本质的哲学科普,其中简洁又不失细节的介绍了人类 ...

  9. 5分钟完全掌握Python协程

    本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理 1. 协程相关的概念 1.1 进程和线程 进程(Process)是应用程序启动的实例,拥有代码.数据 ...

随机推荐

  1. Java利用模板生成pdf并导出

    1.准备工作 (1)Adobe Acrobat pro软件:用来制作导出模板 (2)itext的jar包 2.开始制作pdf模板 (1)先用word做出模板界面 (2)文件另存为pdf格式文件 (3) ...

  2. vue 简易学习

    好记性不如烂笔头 最近公司新出一个框架,采用的是前后端分离的开发方式,后端用的是springboot+mybatis(还有额外的zk.缓存.日志等待),前端采用的是vue+es6,由于以前对vue只知 ...

  3. 第二周Java课堂作业

    演示一: public class EnumTest { public static void main(String[] args) { Size s=Size.SMALL; Size t=Size ...

  4. 如何编写正确且高效的 OpenResty 应用

    本文内容,由我在 OpenResty Con 2018 上的同名演讲的演讲稿整理而来. PPT 可以在 这里 下载,因为内容比较多,我就不在这里一张张贴出来了.有些内容需要结合 PPT 才能理解,请多 ...

  5. 本地代码库,提交远程git

    1.在git上新建项目,并填好相关信息 2.新建成功后,复制项目地址 3.idea新建本地仓库 4.Add所有文件,然后提交(commit) 5.先打开push界面,设置git远程地址,然后关掉,先p ...

  6. k8s-部署及介绍

    Kubernetes主要功能:  数据卷 Pod中容器之间共享数据,可以使用数据卷.  应用程序健康检查 容器内服务可能进程堵塞无法处理请求,可以设置监控检查策略保证应用健壮性.  复制应用程序 ...

  7. Dreamoon and Strings CodeForces - 477C (字符串dp)

    大意: 给定字符串$s$, $p$, 对于$0\le x\le |s|$, 求$s$删除$x$个字符后, $p$在$s$中的最大出现次数. 显然答案是先递增后递减的, 那么问题就转化求最大出现次数为$ ...

  8. MySQL中的数据库对象

    1.数据库中一般包含下列对象 表.约束.索引.触发器.序列.视图: 可以使用图形用户界面或通过显式执行语句来创建这些数据库对象.用于创建这些数据库对象的语句称为“数据定义语言”(DDL),它们通常以关 ...

  9. Windows authentication for WCF web services error

    WCF Web服务的Windows身份验证在部署到IIS时,默认网站添加应用程序的方式.浏览运行.svc文件报错. 错误代码: The authentication schemes configure ...

  10. 怎样理解 instanceof

    instanceof 运算符用来判断一个对象在其原型链中是否存在一个构造函数的 prototype 属性. 也就是说, instanceof 判断的实际上是某个对象是否为某个构造函数的实例, 因为es ...