Python协程的引入与原理分析
相关概念
- 并发:指一个时间段内,有几个程序在同一个cpu上运行,但是任意时刻只有一个程序在cpu上运行。比如说在一秒内cpu切换了100个进程,就可以认为cpu的并发是100。
- 并行:值任意时刻点上,有多个程序同时运行在cpu上,可以理解为多个cpu,每个cpu独立运行自己程序,互不干扰。并行数量和cpu数量是一致的。
我们平时常说的高并发而不是高并行,是因为cpu的数量是有限的,不可以增加。
形象的理解:cpu对应一个人,程序对应喝茶,人要喝茶需要四个步骤(可以对应程序需要开启四个线程):1烧水,2备茶叶,3洗茶杯,4泡茶。
并发方式:烧水的同时做好2备茶叶,3洗茶杯,等水烧好之后执行4泡茶。这样比顺序执行1234要省时间。
并行方式:叫来四个人(开启四个进程),分别执行任务1234,整个程序执行时间取决于耗时最多的步骤。
- 同步 (注意同步和异步只是针对于I/O操作来讲的)值调用IO操作时,必须等待IO操作完成后才开始新的的调用方式。
- 异步 指调用IO操作时,不必等待IO操作完成就开始新的的调用方式。
- 阻塞 指调用函数的时候,当前线程被挂起。
- 非阻塞 指调用函数的时候,当前线程不会被挂起,而是立即返回。
IO多路复用
sllect, poll, epoll都是IO多路复用的机制。IO多路复用就是通过这样一种机制:一个进程可以监听多个描述符,一旦某个描述符就绪(一般是读就绪和写就绪),能够通知程序进行相应的操作。但select,poll,epoll本质上都是同步IO,因为他们都需要在读写事件就绪后自己负责进行读写(即将数据从内核空间拷贝到应用缓存)。也就是说这个读写过程是阻塞的。而异步IO则无需自己负责读写,异步IO的实现会负责把数据从内核拷贝到用户空间。
select
select函数监听的文件描述符分三类:writefds、readfds、和exceptfds。调用后select函数会阻塞,直到描述符就绪(有数据可读、写、或者有except)或者超时(timeout指定等待时间,如果立即返回则设置为null),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
- 优点:良好的跨平台性(几乎所有的平台都支持)
- 缺点:单个进程能够监听的文件描述符数量存在最大限制,在linux上一般为1024,可以通过修改宏定义甚至重新编译内核来提升,但是这样也会造成效率降低。
poll
不同于select使用三个位图来表示fdset的方式,poll使用的是pollfd的指针实现
pollfd结构包含了要监听的event和发生的event,不再使用select“参数-值”传递的方式。同时pollfd并没有最大数量限制(但是数量过大之后性能也是会下降)。和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在同一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会下降。
epoll
epoll是在linux2.6内核中国提出的,(windows不支持),是之前的select和poll增强版。相对于select和poll来说,epoll更加灵活,没有描述符的限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的时间存放到内核的一个时间表中。这样在用户控件和内核控件的coppy只需要一次。
如何选择?
①在并发高同时连接活跃度不是很高的请看下,epoll比select好(网站或web系统中,用户请求一个页面后随时可能会关闭)
②并发性不高,同时连接很活跃,select比epoll好。(比如说游戏中数据一但连接了就会一直活跃,不会中断)
省略章节:由于在用到select的时候需要嵌套多层回调函数,然后印发一系列的问题,如可读性差,共享状态管理困难,出现异常排查复杂,于是引入协程,既操作简单,速度又快。
协程
对于上面的问题,我们希望去解决这样几个问题:
- 采用同步的方式去编写异步的代码,使代码的可读性高,更简便。
- 使用单线程去切换任务(就像单线程间函数之间的切换那样,速度超快)
(1)线程是由操作系统切换的,单线程的切换意味着我们需要程序员自己去调度任务。
(2)不需要锁,并发性高,如果单线程内切换函数,性能远高于线程切换,并发性更高。
例如我们在做爬虫的时候:
- def get_url(url):
- html = get_html(url) # 此处网络下载IO操作比较耗时,希望切换到另一个函数去执行
- infos = parse_html(html)
- # 下载url中的html
- def get_html(url):
- pass
- # 解析网页
- def parse_html(html):
- pass
意味着我们需要一个可以暂停的函数,对于此函数可以向暂停的地方穿入值。(回忆我们的生成器函数就可以满足这两个条件)所以就引入了协程。
生成器进阶
- 生成器不仅可以产出值,还可以接收值,用send()方法。注意:在调用send()发送非None值之前必须先启动生成器,可以用①next()②send(None)两种方式激活
- def gen_func():
- html = yield 'http://www.baidu.com' # yield 前面加=号就实现了1:可以产出值2:可以接受调用者传过来的值
- print(html)
- yield 2
- yield 3
- return 'bobby'
- if __name__ == '__main__':
- gen = gen_func()
- url = next(gen)
- print(url)
- html = 'bobby'
- gen.send(html) # send方法既可以将值传递进生成器内部,又可以重新启动生成器执行到下一yield位置。
- 打印结果:
- http://www.baidu.com
- bobby
- close()方法。
- def gen_func():
- yield 'http://www.baidu.com' # yield 前面加=号就实现了1:可以产出值2:可以接受调用者传过来的值
- yield 2
- yield 3
- return 'bobby'
- if __name__ == '__main__':
- gen = gen_func()
- url = next(gen)
- gen.close()
- next(gen)
- 输出结果:
- StopIteration
特别注意:调用close.()之后, 生成器在往下运行的时候就会产生出一个GeneratorExit,单数如果用try捕获异常的话,就算捕获了遇到后面还有yield的话,还是不能往下运行了,因为一旦调用close方法生成器就终止运行了(如果还有next,就会会产生一个异常)所以我们不要去try捕捉该异常。(此注意可以先忽略)
- def gen_func():
- try:
- yield 'http://www.baidu.com'
- except GeneratorExit:
- pass
- yield 2
- yield 3
- return 'bobby'
- if __name__ == '__main__':
- gen = gen_func()
- print(next(gen))
- gen.close()
- next(gen)
- 输出结果:
- RuntimeError: generator ignored GeneratorExit
- 调用throw()方法。用于抛出一个异常。该异常可以捕捉忽略。
- def gen_func():
- yield 'http://www.baidu.com' # yield 前面加=号就实现了1:可以产出值2:可以接受调用者传过来的值
- yield 2
- yield 3
- return 'bobby'
- if __name__ == '__main__':
- gen = gen_func()
- print(next(gen))
- gen.throw(Exception, 'Download Error')
- 输出结果:
- Download Error
yield from
先看一个函数:from itertools import chain
- from itertools import chain
- my_list = [1,2,3]
- my_dict = {'frank':'yangchao', 'ailsa':'liuliu'}
- for value in chain(my_list, my_dict, range(5,10)): chain()方法可以传入多个可迭代对象,然后分别遍历之。
- print(value)
- 打印结果:
- 1
- 2
- 3
- frank
- ailsa
- 5
- 6
- 7
- 8
- 9
此函数可以用yield from 实现:yield from功能 1:从一个可迭代对象中将值逐个返回。
- my_list = [1,2,3]
- my_dict = {'frank':'yangchao', 'ailsa':'liuliu'}
- def chain(*args, **kwargs):
- for itemrable in args:
- yield from itemrable
- for value in chain(my_list, my_dict, range(5,10)):
- print(value)
看如下代码:
- def gen():
- yield 1
- def g1(gen):
- yield from gen
- def main():
- g = g1(gen)
- g.send(None)
代码分析:此代码中main调用了g1, main就叫作调用方, g1叫做委托方, gen 叫做子生成器yield from将会在调用方main与子生成器gen之间建立一个双向通道。(意味着可以直接越过委托方)
例子:当委托方middle()中使用yield from 的时候,调用方main直接和子生成器sales_sum形成数据通道。
- final_result = {}
- def sales_sum(pro_name):
- total = 0
- nums = []
- while True:
- x = yield
- print(pro_name+'销量', x)
- if not x:
- break
- total += x
- nums.append(x)
- return total, nums #程序运行到return的时候,会将return的返回值返回给委托方,即middle中的final_result[key]
- def middle(key):
- while True: #相当于不停监听sales_sum是否有返回数据,(本例中有三次返回)
- final_result[key] = yield from sales_sum(key)
- print(key +'销量统计完成!!')
- def main():
- data_sets = {
- '面膜':[1200, 1500, 3000],
- '手机':[88, 100, 98, 108],
- '衣服':[280, 560,778,70],
- }
- for key, data_set in data_sets.items():
- print('start key', key)
- m = middle(key)
- m.send(None) # 预激生成器
- for value in data_set:
- m.send(value)
- m.send(None)# 发送一个None使sales_sum中的x值为None退出while循环
- print(final_result)
- if __name__ == '__main__':
- main()
- 结果:
- start key 面膜
- 面膜销量 1200
- 面膜销量 1500
- 面膜销量 3000
- 面膜销量 None
- 面膜销量统计完成!!
- start key 手机
- 手机销量 88
- 手机销量 100
- 手机销量 98
- 手机销量 108
- 手机销量 None
- 手机销量统计完成!!
- start key 衣服
- 衣服销量 280
- 衣服销量 560
- 衣服销量 778
- 衣服销量 70
- 衣服销量 None
- 衣服销量统计完成!!
- {'面膜': (5700, [1200, 1500, 3000]), '手机': (394, [88, 100, 98, 108]), '衣服': (1688, [280, 560, 778, 70])}
也许有人会好奇,为什么不能直接用main()函数直接去调用sales_sum呢?加一个委托方使代码复杂化了。看以下直接用main()函数直接去调用sales_sum代码:
- def sales_sum(pro_name):
- total =
- nums = []
- while True:
- x = yield
- print(pro_name+'销量', x)
- if not x:
- break
- total +=
- nums.append(x)
- return total, nums
- if __name__ == '__main__':
- my_gen = sales_sum('面膜')
- my_gen.send(None)
- my_gen.send()
- my_gen.send()
- my_gen.send()
- my_gen.send(None)
- 输出结果:
- 面膜销量
- 面膜销量
- 面膜销量
- 面膜销量 None
- Traceback (most recent call last):
- File "D:/MyCode/Cuiqingcai/Flask/test01.py", line , in <module>
- my_gen.send(None)
- StopIteration: (, [, , ])
从上述代码可以看出,即使数据return结果出来了,还是会返回一个exception,由此可以看出yield from的一个最大优点就是当子生成器运行时候出现异常,yield from可以直接自动处理这些异常。
yield from 功能总结:
- 子生成器生产的值,都是直接给调用方;调用发通过.send()发送的值都是直接传递给子生成器,如果传递None,会调用子生成器的next()方法,如果不是None,会调用子生成器的sen()方法。
- 子生成器退出的时候,最后的return EXPR,会触发一个StopIteration(EXPR)异常
- yield from 表达式的值,是子生成器终止时,传递给StopIteration异常的第一个参数。
- 如果调用的时候出现了StopIteration异常,委托方生成器恢复运行,同时其他的异常向上冒泡。
- 传入委托生成器的异常里,除了GeneratorExit之后,其他所有异常全部传递给子生成器的.throw()方法;如果调用.throw()的时候出现StopIteration异常,那么就恢复委托生成器的运行,其他的异常全部向上冒泡
- 如果在委托生成器上调用.close()或传入GeneratorExit异常,会调用子生成器的.close()方法,没有就不调用,如果在调用.close()时候抛出了异常,那么就向上冒泡,否则的话委托生成器跑出GeneratorExit 异常。
Python协程的引入与原理分析的更多相关文章
- python协程详解
目录 python协程详解 一.什么是协程 二.了解协程的过程 1.yield工作原理 2.预激协程的装饰器 3.终止协程和异常处理 4.让协程返回值 5.yield from的使用 6.yield ...
- Python协程与Go协程的区别二
写在前面 世界是复杂的,每一种思想都是为了解决某些现实问题而简化成的模型,想解决就得先面对,面对就需要选择角度,角度决定了模型的质量, 喜欢此UP主汤质看本质的哲学科普,其中简洁又不失细节的介绍了人类 ...
- python协程详解,gevent asyncio
python协程详解,gevent asyncio 新建模板小书匠 #协程的概念 #模块操作协程 # gevent 扩展模块 # asyncio 内置模块 # 基础的语法 1.生成器实现切换 [1] ...
- Unity协程(Coroutine)原理深入剖析
Unity协程(Coroutine)原理深入剖析 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 其实协程并没有那么复杂,网上很多地方都说是多 ...
- Unity协程(Coroutine)原理深入剖析(转载)
记得去年6月份刚开始实习的时候,当时要我写网络层的结构,用到了协程,当时有点懵,完全不知道Unity协程的执行机制是怎么样的,只是知道函数的返回值是IEnumerator类型,函数中使用yield r ...
- 再议Python协程——从yield到asyncio
协程,英文名Coroutine.前面介绍Python的多线程,以及用多线程实现并发(参见这篇文章[浅析Python多线程]),今天介绍的协程也是常用的并发手段.本篇主要内容包含:协程的基本概念.协程库 ...
- day-5 python协程与I/O编程深入浅出
基于python编程语言环境,重新学习了一遍操作系统IO编程基本知识,同时也学习了什么是协程,通过实际编程,了解进程+协程的优势. 一.python协程编程实现 1. 什么是协程(以下内容来自维基百 ...
- 从yield 到yield from再到python协程
yield 关键字 def fib(): a, b = 0, 1 while 1: yield b a, b = b, a+b yield 是在:PEP 255 -- Simple Generator ...
- 【转】Unity协程(Coroutine)原理深入剖析
Unity协程(Coroutine)原理深入剖析 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 记得去年6月份刚开始实习的时候,当时要我写网 ...
随机推荐
- Python开发:Python2和Python3的共存和切换使用
从python2到python3,这两个版本可以说是从语法.编码等多个方面上都有很大的差别.为了不带入过多的累赘,Python 3.0在设计的时候没有考虑向下相容,也就是说许多针对早期Python2版 ...
- 从壹开始微服务 [ DDD ] 之八 ║剪不断理还乱的 值对象和Dto
缘起 哈喽大家周四好,时间是过的真快,这几天一直忙着在公司的项目,然后带带新人,眼看这周要过去了,还是要抽出时间学习学习,这些天看到群里的小伙伴也都在忙着新学习,还是很开心的,至少当时的初衷已经达到了 ...
- sersync自动化同步部署
目录 0. 前提: 1. 部署rsync server服务 1.1 配置rsync配置文件 1.2 rsync配置文件说明: 1.3 创建密码文件修改权限600 1.4 创建提示文件(可有可无) 1. ...
- Java集合详解4:HashMap和HashTable
今天我们来探索一下HashMap和HashTable机制与比较器的源码. 具体代码在我的GitHub中可以找到 https://github.com/h2pl/MyTech 喜欢的话麻烦star一下哈 ...
- js数组遍历(for in ,for of ,map,foreach,filter)的区别
一.for in 和for of 的区别 1.for in 遍历数组时,索引实际上是字符串类型的数字,不能进行运算,我们来输出一下: let arr = [1,3,5,4] for (let inde ...
- Mysql、SqlServer、Oracle三大数据库的区别
一.MySQL 优点: 体积小.速度快.总体拥有成本低,开源: 支持多种操作系统: 是开源数据库,提供的接口支持多种语言连接操作 : MySQL的核心程序采用完全的多线程编程.线程是轻量级的进程,它可 ...
- Java 设置PDF文档背景色
一般生成的PDF文档默认的文档底色为白色,我们可以通过一定方法来更改文档的背景色,以达到文档美化以及保护双眼的作用. 以下内容提供了Java编程来设置PDF背景色的方法.包括: 设置纯色背景色 设置图 ...
- Uncaught ReferenceError: jQuery is not defined
页面调试时,明明引入了JQ文件,却一直提示Uncaught ReferenceError: jQuery is not defined错误. 转自:http://blog.csdn.net/baicp ...
- 三、Snapman多人协作电子表格之——软件的基本功能
Snapman多人协作电子表格是一个即时工作系统. 一.SnapmanServer服务端 SnapmanServer服务端在安装Snapman软件一起自带,是一个小巧的控制台程序SnapmanServ ...
- Android为TV端助力 使用shared注意事项
不要存放大的key和value!我就不重复三遍了,会引起界面卡.频繁GC.占用内存等等,好自为之! 毫不相关的配置项就不要丢在一起了!文件越大读取越慢,不知不觉就被猪队友给坑了:蓝后,放进defalu ...