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. 使用单线程去切换任务(就像单线程间函数之间的切换那样,速度超快)

      (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 = 0
 nums = []
 while True:
  x = yield
  print(pro_name+'销量', x)
  if not x:
   break
  total += 1
  nums.append(x)
 return total, nums

if __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
面膜销量 None
Traceback (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 协程的资料请关注我们其它相关文章!

(0)

相关推荐

  • python 协程 gevent原理与用法分析

    本文实例讲述了python 协程 gevent原理与用法.分享给大家供大家参考,具体如下: gevent greenlet已经实现了协程,但是这个还的人工切换,是不是觉得太麻烦了,不要捉急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent 其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络.文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行. 由于IO操

  • 浅谈Python协程

    协程 协程,又称微线程,纤程.英文名Coroutine.一句话说明什么是线程:协程是一种用户态的轻量级线程. 协程拥有自己的寄存器上下文和栈.协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈.因此: 协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置. 协程的好处: 无需线程上下文切换的开销 无需原子操作锁定及同步的开销 "原子操作(atomic o

  • Python异步编程之协程任务的调度操作实例分析

    本文实例讲述了Python异步编程之协程任务的调度操作.分享给大家供大家参考,具体如下: 我们知道协程是异步进行的,碰到IO阻塞型操作时需要调度其他任务,那么这个调度规则或者是算法是怎样的呢?现在有以下几个疑问: 1.多个任务准备好,需要运行时,优先执行哪一个? 2.一个任务运行时,如果别的任务准备好了,是否需要中断当前任务呢? 在网上找了很多资料,也无法找到相关的资料,于是编写了几个简单的程序,查看任务的执行过程. 根据Python的asyncio我们可以编写一个简单的程序: import a

  • python 使用事件对象asyncio.Event来同步协程的操作

    事件对象asyncio.Event是基于threading.Event来实现的. 事件可以一个信号触发多个协程同步工作, 例子如下: import asyncio import functools def set_event(event): print('setting event in callback') event.set() async def coro1(event): print('coro1 waiting for event') await event.wait() print(

  • 协程Python 中实现多任务耗资源最小的方式

    协程,又称微线程,纤程.英文名 Coroutine. 协程是 Python 中另外一种实现多任务的方式,只不过比线程更小,占用更小执行单元(理解为需要的资源). 为啥说它是一个执行单元,因为它自带 CPU 上下文.这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程. 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的. 通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的

  • Python gevent协程切换实现详解

    一.背景 大家都知道gevent的机制是单线程+协程机制,当遇到可能会阻塞的操作时,就切换到可运行的协程中继续运行,以此来实现提交系统运行效率的目标,但是具体是怎么实现的呢?让我们直接从代码中看一下吧. 二.切换机制 让我们从socket的send.recv方法入手: def recv(self, *args): while 1: try: return self._sock.recv(*args) except error as ex: if ex.args[0] != EWOULDBLOCK

  • python使用协程实现并发操作的方法详解

    本文实例讲述了python使用协程实现并发操作的方法.分享给大家供大家参考,具体如下: 协程 协程是一种用户态的轻量级线程,又称微线程. 协程拥有自己的寄存器上下文和栈,调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈.因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置. 优点: 无需线程上下文切换的开销 无需原子操作锁定及同步的开销 方便切换控制

  • Python使用monkey.patch_all()解决协程阻塞问题

    直接参考以下实例,采用协程访问三个网站 由于IO操作非常耗时,程序经常会处于等待状态 比如请求多个网页有时候需要等待,gevent可以自动切换协程 遇到阻塞自动切换协程,程序启动时执行monkey.patch_all()解决 # 由于IO操作非常耗时,程序经常会处于等待状态 # 比如请求多个网页有时候需要等待,gevent可以自动切换协程 # 遇到阻塞自动切换协程,程序启动时执行monkey.patch_all()解决 # 首行添加下面的语句即可 from gevent import monke

  • python 多进程和协程配合使用写入数据

    一.需求分析 有一批key已经写入到3个txt文件中,每一个txt文件有30万行记录. 现在需要读取这些txt文件,判断key是否在数据仓库中.(redis或者mysql) 为空的记录,需要写入到日志文件中! 任务分工 1. 使用多进程技术,每一个进程读取一个txt文件 2. 使用协程技术,批量读取txt文件记录.比如一次性读取 2000条记录 注意:打开文件操作,最好在一个进程中,重复打开文件,会造成系统资源浪费! 二.完整代码 #!/usr/bin/env python3 # coding:

  • Python手动或自动协程操作方法解析

    1.手动协程操作: # pip install gevent from greenlet import greenlet def test(): print('He ') gr2.switch() # 切换到test2 print('a ') gr2.switch() def test2(): print('is ') gr1.switch() print('student.') gr1 = greenlet(test) # 创建一个协程 gr2 = greenlet(test2) gr1.sw

  • python3爬虫中异步协程的用法

    1. 前言 在执行一些 IO 密集型任务的时候,程序常常会因为等待 IO 而阻塞.比如在网络爬虫中,如果我们使用 requests 库来进行请求的话,如果网站响应速度过慢,程序一直在等待网站响应,最后导致其爬取效率是非常非常低的. 为了解决这类问题,本文就来探讨一下 Python 中异步协程来加速的方法,此种方法对于 IO 密集型任务非常有效.如将其应用到网络爬虫中,爬取效率甚至可以成百倍地提升. 注:本文协程使用 async/await 来实现,需要 Python 3.5 及以上版本. 2.

  • Python协程 yield与协程greenlet简单用法示例

    本文实例讲述了Python协程 yield与协程greenlet简单用法.分享给大家供大家参考,具体如下: 协程 协程,又称微线程,纤程.英文名Coroutine. 协程是啥 协程是python个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源). 为啥说它是一个执行单元,因为它自带CPU上下文.这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程. 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的. 通俗的理解:在一个线程中的某个函数,可以在任

随机推荐