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中的htmldef 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.combobby 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 chainmy_list = [1,2,3]my_dict = {’frank’:’yangchao’, ’ailsa’:’liuliu’}for value in chain(my_list, my_dict, range(5,10)): chain()方法可以传入多个可迭代对象,然后分别遍历之。 print(value)打印结果:123frankailsa56789
此函数可以用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 itemrablefor value in chain(my_list, my_dict, range(5,10)): print(value)
看如下代码:
def gen(): yield 1def g1(gen): yield from gendef 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 = 0 nums = [] while True: x = yield print(pro_name+’销量’, x) if not x: break total += 1 nums.append(x) return total, numsif __name__ == ’__main__’: my_gen = sales_sum(’面膜’) my_gen.send(None) my_gen.send(1200) my_gen.send(1500) my_gen.send(3000) my_gen.send(None)输出结果:面膜销量 1200面膜销量 1500面膜销量 3000面膜销量 NoneTraceback (most recent call last): File 'D:/MyCode/Cuiqingcai/Flask/test01.py', line 56, in <module> my_gen.send(None)StopIteration: (3, [1200, 1500, 3000])
从上述代码可以看出,即使数据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 协程的资料请关注好吧啦网其它相关文章!
相关文章:
