理解协程并搞定协程相关面试题
一、什么是协程
协程:实现协作式多任务,可以在程序执行内部中断,转而执行其他协程。
比如我们编写子程序(或者说函数),通常是利用“调用”来实现从 A 跳去 B,B 跳去 C,如果想回来调用方,必须等被调用方执行完才行,整个调用过程是通过栈实现的。而协程是运行子程序的过程中“中断”,转而执行其他子程序,再在适当的时候返回来接着运行。
二、协程与线程的区别
协程相比于线程的优势: 1、协程效率比线程高。线程间切换需要开销,而协程间切换是由程序自身控制的,不需要开销。 2、协程不需要多线程的锁机制。协程是在一个线程内进行切换,所以不存在同时写变量冲突,不需要给共享资源加锁,只需要判断状态。
PS:如果想使用多CPU的话,可以使用进程+协程。
三、协程的实现
协程是通过yield实现的,所以协程是生成器,可以通过 next 调用。
def simple(a): print("----start----") r = yield a print('----r------' + str(r)) >>> my_simple = simple(5) >>> my_simple <generator object simple at 0x10f9242b0> >>> next(my_simple) ----start---- 5 >>> my_simple.send(8) ----r------8 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
以上是一个简单的协程例子,可以看到,my_simple是一个生成器实例,需要使用next()方法或send(None)去预激协程,协程运行到yield的时候停止,当使用send()方法给yield赋值时,程序继续往下运行,并抛出StopIteration异常。
四、协程返回值
在python3.3版本后,协程可以有返回值。
def simple(a): print("----start----") r = yield a print('----r------' + str(r)) return r >>> my_simple = simple(5) >>> next(my_simple) ----start---- 5 >>> my_simple.send(8) ----r------8 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration: 8
可以看到,在程序运行完后,异常对象StopIteration的value属性保存着返回的值。
五、使用yield from获取协程返回值
对于yield from来说,解释器不仅会捕获StopIteration异常,还会把value属性的值作为yield from表达式的值。
yield from主要功能是打开双向通道,把最外层的调用方和最内层的子生成器连接起来。
# 子生成器 def total_num(): total = 0 while True: num = yield if num == None: break total += num return total # 委派生成器 def send_num(result, key): while True: result[key] = yield from total_num() # 调用方 def main(data): result = {} for key, nums in data.items(): group = send_num(result, key) next(group) for num in nums: group.send(num) group.send(None) print(result) data = { 'nums1': [12, 34, 23, 4, 35, 34, 34, 55], 'nums2': [22, 44, 33, 24, 33, 24, 4, 15], 'nums3': [32, 54, 43, 41, 31, 44, 24, 25], 'nums4': [42, 64, 53, 43, 37, 74, 74, 35], 'nums5': [52, 74, 63, 46, 39, 84, 44, 45] } if __name__ == "__main__": main(data)
输出结果如下:
{'nums1': 231, 'nums2': 199, 'nums3': 294, 'nums4': 422, 'nums5': 447}
委派生成器作为双向管道把调用方和子生成器连接起来,委派生成器在yield from表达式处暂停时,调用方通过send()方法把数据传给子生成器,子生成器再把产出值发送给调用方,子生成器返回后,会抛出StopIteration异常,并把返回值添加到异常的value属性上,此时你异常生成器会恢复,并获取异常的value值作为yield from表达式的值。
委派生成器相当于管道,所以可以把任意个委派生成器连起来:委派生成器连接的子生成器是一个委派生成器,以此类推,直到遇到一个使用yield的生成器或可迭代对象。
六、总结
1、协程是用于控制程序中断,它与函数调用不同
2、协程是在单线程里可处理多任务,相比多线程节省了线程切换的开销
3、协程通过 yield 关键字实现,它也是一种生成器
4、协程调用方可通过 send() 方法给被调用方发送值。协程的开启需要预激,预激方法是:send(None)或者next()
5、协程在 python3.3 之后是有返回值的,返回值会放在 StopIteration 异常的 value 里
6、yield from 的作用是在生成器里调用子生成器,可以优化一个嵌套 for 循环等复杂代码
7、协程有四种状态:GEN_CREATED(等待开始执行)、GEN_RUNNING(解释器正在执行)、GEN_SUSPENDED(在yield表达式处停止)、GEN_CLOSED(执行结束)。通过 inspect.getgeneratorstate(...) 函数可获取
8、结束协程的两种方式:generator.throw 或 generator.close
七、相关面试题
上面介绍了协程的概念,相对应的这里提供几个关于协程的面试题目。
理论结合“面试”
1、什么是进程、线程、协程?
答案要点: a、进程是资源分配,每个进程拥有独立的资源空间,因为进程不共享资源,所以就涉及到进程间通信的方式,常见的方式有:消息队列、管道、信号量、socket套接字等。(这里会引申出几个面试题:进程间有通信方式有哪些?-> 使用过哪些消息队列?)
b、线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。线程是在进程下,所以同一进程下的多个线程是能共享资源的。线程能共享的资源有:堆、全局变量、文件描述符和信号处理等,不共享的资源:栈、寄存器等(这里会引申出的面试题:多线程怎么实现?-> 多线程并发问题 -> 多线程共享哪些资源)
c、协程是单线程下实现多任务,它通过 yield 关键字来实现,能有效地减少多线程之间切换的开销。它是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
2、协程有什么优缺点? 答案要点 a、协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),性能得到了很大的提升,不会像线程切换那样消耗资源。
b、缺点:异步代码,可能不那么容易理解和调度
3、下面代码输出结果是什么?
def test(): print("1"*30) yield "A" print("A"*30) yield "B" print("B"*30) t = test() # 1 print(next(t)) # 2 print(next(t)) # 3 print(next(t)) # 4
答案要点: 这其实是属于生成器的一个题目,输出如下: 1:没有任何输出,它不会执行 print("1"*30),只会返回一个生成器 2:输出 "111...111"(30个),同时打印返回值"A" 3:输出 "111...111"(30个),同时打印返回值"B" 4:异常、StopIteration
4、请写一个简单的协程示例 或 利用协程实现一个 生产者消费者 模式
面试一般其实比较少说让面试者手写一个协程代码,不过之前确实有遇到过让手写一个利用协程实现生产者消费者模式的。示例代码如下(代码来源于:https://www.liaoxuefeng.com/wiki/897692888725344/923057403198272):
import time def consumer(): r = '' while True: n = yield r if not n: return print('[CONSUMER] Consuming %s...' % n) time.sleep(1) r = '200 OK' def produce(c): c.next() n = 0 while n < 5: n = n + 1 print('[PRODUCER] Producing %s...' % n) r = c.send(n) print('[PRODUCER] Consumer return: %s' % r) # 终止协程 # 终止协程的方式:generator.throw 或者 generator.close c.close() if __name__=='__main__': c = consumer() produce(c)
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Elasticsearch搜索调优权威指南 (2/3)
本文首发于 vivo互联网技术 微信公众号 https://mp.weixin.qq.com/s/AAkVdzmkgdBisuQZldsnvg 英文原文:https://qbox.io/blog/elasticsearch-search-tuning-part-2 作者:Adam Vanderbush 译者:杨振涛 目录 预索引数据 映射 避免使用脚本 强制合并只读索引 Elasticsearch搜索调优权威指南,是QBOX在其博客上发布的系列文章之一,本文是该系列的第二篇,主要介绍了索引预处理、mapping建立、避免脚本的使用、索引段合并等搜索性能相关的调优方法。 本文是Elasticsearch搜索调优系列文章3篇中的第2篇,第1篇参考这里(点击)。本系列教程旨在更进一步讨论针对Elasticsearch 5.0及以上版本的搜索调优技术、策略及建议。 1.预索引数据 为了优化数据的索引方式,应当在查询中预置一些模式。比如,如果所有文档都有一个叫 price 的价格字段,并且大部分查询在一个固定范围列表上执行 range 聚合,那么就可以通过预索引范围到索引中并使用一个 terms...
- 下一篇
关于 Java 类加载器的这一点,市面上没有任何一本图书讲到
一、一个程序员的思考 大家都知道,Tomcat 处理业务,靠什么?最终是靠我们自己编写的 Servlet。你可能说你不写 servlet,你用 spring MVC,那也是人家帮你写好了,你只需要配置就行。在这里,有一个边界,Tomcat 算容器,容器的相关 jar 包都放在它自己的 安装目录的 lib 下面; 我们呢,算是业务,算是webapp,我们的 servlet ,不管是自定义的,还是 spring mvc 的DispatcherServlet,都是放在我们的 war 包里面 WEB-INF/lib下。 看过前面文章的同学是晓得的, 这二者是由不同的类加载器加载的。在 Tomcat 的实现中,会委托 webappclassloader 去加载WAR 包中的 servlet ,然后 反射生成对应的 servlet。后续有请求来了,调用生成的 servlet 的 service 方法即可。 在 org.apache.catalina.core.StandardWrapper#loadServlet 中,即负责 生成 servlet: org.apache.catalina.core...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8编译安装MySQL8.0.19
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装