python中的协程深入理解


Posted in Python onJune 10, 2019

先介绍下什么是协程:

协程,又称微线程,纤程,英文名Coroutine。协程的作用,是在执行函数A时,可以随时中断,去执行函数B,然后中断继续执行函数A(可以自由切换)。但这一过程并不是函数调用(没有调用语句),这一整个过程看似像多线程,然而协程只有一个线程执行。

是不是有点没看懂,没事,我们下面会解释。要理解协程是什么,首先需要理解yield,这里简单介绍下,yield可以理解为生成器,yield item这行代码会产出一个值,提供给next(...)的调用方; 此外,还会作出让步,暂停执行生成器,让调用方继续工作,直到需要使用另一个值时再调用next()。调用方会从生成器中拉取值,但是在协程中,yield关键字一般是在表达式右边(如,data=yield),协程可以从调用方接收数据,也可以产出数据,下面看一个简单的例子:

>>> def simple_coroutine():
...  print('coroutine start')
...  x = yield
...  print('coroutine recive:',x)
...  
>>> my_co=simple_coroutine()
>>> my_co
<generator object simple_coroutine at 0x1085174f8>
>>> next(my_co)
coroutine start
>>> my_co.send(42)
coroutine recive: 42
Traceback (most recent call last):
 File "<input>", line 1, in <module>
StopIteration

其中x = yield就是精髓部分,意思是从客户端获取数据,产出None,因为yield关键字右边没有表达式, 而协程在创建完成之后,是没有启动的,没有在yield处暂停,所以需要调用next()函数,启动协程,在调用my_co.send(42)之后,协程定义体中的yield表达式会计算出42,现在协程恢复,一直运行到下一个yield表达式,或者终止,在最后,控制权流动到协程定义体的末尾,生成器抛出StopIteration异常。

协程有四个状态,如下:

  • 'GEN_CREATED' 等待开始执行。
  • 'GEN_RUNNING' 解释器正在执行。
  • 'GEN_SUSPENDED' 在 yield 表达式处暂停。
  • 'GEN_CLOSED' 执行结束。

当前状态可以使用inspect.getgeneratorstate来确定,如下:

>>> import inspect
>>> inspect.getgeneratorstate(my_co)
'GEN_CLOSED'

这里再解释下next(my_co),如果在创建好协程对象之后,立即把None之外的值发送给它,会出现如下错误:

>>> my_co=simple_coroutine()
>>> my_co.send(42)
Traceback (most recent call last):
 File "<input>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator
>>> my_co=simple_coroutine()
>>> my_co.send(None)
coroutine start

最先调用 next(my_co) 函数这一步通常称为“预激”(prime)协程(即,让协程向前执行到第一个 yield 表达式,准备好作为活跃的协程使用)。

再参考下面这个例子:

>>> def simple_coro2(a):
...  print('-> Started: a =', a)
...  b = yield a
...  print('-> Received: b =', b)
...  c = yield a + b
...  print('-> Received: c =', c)
...  
>>> my_coro2 = simple_coro2(14)
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(my_coro2)
'GEN_CREATED'
>>> next(my_coro2) # 协程执行到`b = yield a`处暂停,等待为b赋值,
-> Started: a = 14
14
>>> getgeneratorstate(my_coro2) 
'GEN_SUSPENDED' #从状态也可以看到,当前是暂停状态。
>>> my_coro2.send(28) #将28发送到协程,计算yield表达式,并把结果绑定到b,产出a+b的值,然后暂停。
-> Received: b = 28
42
>>> my_coro2.send(99)
-> Received: c = 99
Traceback (most recent call last):
 File "<input>", line 1, in <module>
StopIteration
>>> getgeneratorstate(my_coro2)
'GEN_CLOSED'

simple_coro2的执行过程如下图所示:

python中的协程深入理解

  • 调用next(my_coro2),打印第一个消息,然后执行yield a,产出数字 14。
  • 调用my_coro2.send(28),把28赋值给b,打印第二个消息,然后执行yield a + b,产 出数字 42。
  • 调用my_coro2.send(99),把 99 赋值给 c,打印第三个消息,协程终止。

说了这么多,我们为什么要用协程呢,下面我们再看看它的优势是什么:

  • 执行效率极高,因为子程序切换(函数)不是线程切换,由程序自身控制,没有切换线程的开销。所以与多线程相比,线程的数量越多,协程性能的优势越明显。
  • 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在控制共享资源时也不需要加锁,因此执行效率高很多。

说明:协程可以处理IO密集型程序的效率问题,但是处理CPU密集型不是它的长处,如要充分发挥CPU利用率可以结合多进程+协程。

下面看最后一个例子,传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。

如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

from bs4 import BeautifulSoup
import requests
from urllib.parse import urlparse

start_url = 'https://www.cnblogs.com'
trust_host = 'www.cnblogs.com'
ignore_path = []
history_urls = []


def parse_html(html):
  soup = BeautifulSoup(html, "lxml")
  print(soup.title)
  links = soup.find_all('a', href=True)
  return (a['href'] for a in links if a['href'])


def parse_url(url):
  url = url.strip()

  if url.find('#') >= 0:
    url = url.split('#')[0]
  if not url:
    return None
  if url.find('javascript:') >= 0:
    return None

  for f in ignore_path:
    if f in url:
      return None
  if url.find('http') < 0:
    url = start_url + url
    return url
  parse = urlparse(url)
  if parse.hostname == trust_host:
    return url


def consumer():
  html = ''
  while True:
    url = yield html
    if url:
      print('[CONSUMER] Consuming %s...' % url)
      rsp = requests.get(url)
      html = rsp.content


def produce(c):
  next(c)

  def do_work(urls):
    for u in urls:
      if u not in history_urls:
        history_urls.append(u)
        print('[PRODUCER] Producing %s...' % u)
        html = c.send(u)
        results = parse_html(html)
        work_urls = (x for x in map(parse_url, results) if x)
        do_work(work_urls)

  do_work([start_url])
  c.close()


if __name__ == '__main__':
  c = consumer()
  produce(c)
  print(len(history_urls))

首先consumer函数是一个generator,在开始执行之后:

  1. 调用next(c)启动生成器;
  2. 进入do_work,这是一个递归调用,其内部将url传递给consumer,由consumer来发出请求,获取到html信息,返回给produce,
  3. produce解析html,获取url数据,继续生产url,
  4. 当所有的url都在history_urls中,也就是说我们已经爬取了所有的url地址,结束递归调用
  5. 调用c.close(),关闭consumer,整个过程结束。

可以看到,我们的整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
从零学Python之引用和类属性的初步理解
May 15 Python
python 网络爬虫初级实现代码
Feb 27 Python
Python工厂函数用法实例分析
May 14 Python
python使用turtle绘制分形树
Jun 22 Python
解决Pycharm运行时找不到文件的问题
Oct 29 Python
python3实现名片管理系统
Nov 29 Python
Python编程flask使用页面模版的方法
Dec 28 Python
学习python可以干什么
Feb 26 Python
Python2与Python3的区别实例总结
Apr 17 Python
Python numpy线性代数用法实例解析
Nov 15 Python
Python线程指南分享
Nov 19 Python
python代码实现扫码关注公众号登录的实战
Nov 01 Python
Python中asyncio模块的深入讲解
Jun 10 #Python
Python中的asyncio代码详解
Jun 10 #Python
Django集成CAS单点登录的方法示例
Jun 10 #Python
详解Python中的测试工具
Jun 09 #Python
Python中函数参数匹配模型详解
Jun 09 #Python
Python程序包的构建和发布过程示例详解
Jun 09 #Python
Python面向对象之继承和多态用法分析
Jun 08 #Python
You might like
利用PHP制作简单的内容采集器的原理分析
2008/10/01 PHP
php用header函数实现301跳转代码实例
2013/11/25 PHP
yii实现model添加默认值的方法(2种方法)
2016/01/06 PHP
用JavaScript和注册表脚本实现右键收藏Web页选中文本
2007/01/28 Javascript
用js实现手把手教你月入万刀(转贴)
2007/11/07 Javascript
js中onload与onunload的使用示例
2013/08/25 Javascript
js生成动态表格并为每个单元格添加单击事件的方法
2014/04/14 Javascript
jQuery中:not选择器用法实例
2014/12/30 Javascript
JavaScript中的Repaint和Reflow用法详解
2015/07/27 Javascript
jQuery实现动态表单验证时文本框抖动效果完整实例
2015/08/21 Javascript
JavaScript 对象字面量讲解
2016/06/06 Javascript
浅析$.getJSON异步请求和同步请求
2016/06/06 Javascript
angularjs中ng-bind-html的用法总结
2017/05/23 Javascript
原生JS实现图片懒加载(lazyload)实例
2017/06/13 Javascript
深入浅析ES6 Class 中的 super 关键字
2017/10/20 Javascript
Nodejs中怎么实现函数的串行执行
2019/03/02 NodeJs
基于JavaScript获取url参数2种方法
2020/04/17 Javascript
Node.js API详解之 string_decoder用法实例分析
2020/04/29 Javascript
JavaScript 函数用法详解【函数定义、参数、绑定、作用域、闭包等】
2020/05/12 Javascript
解决vue.js中settimeout遇到的问题(时间参数短效果不稳定)
2020/07/21 Javascript
解决vue2中使用elementUi打包报错的问题
2020/09/22 Javascript
[00:33]2016完美“圣”典风云人物:Sccc宣传片
2016/12/03 DOTA
python多进程共享变量
2016/04/06 Python
Python学习笔记之解析json的方法分析
2017/04/21 Python
Django框架之登录后自定义跳转页面的实现方法
2019/07/18 Python
matplotlib grid()设置网格线外观的实现
2021/02/22 Python
css3实现顶部社会化分享按钮示例
2014/05/06 HTML / CSS
html5 分层屏幕适配的方法
2018/03/16 HTML / CSS
Spartoo瑞典:鞋子、包包和衣服
2018/09/15 全球购物
传媒专业推荐信范文
2013/11/23 职场文书
企业承诺书格式
2014/05/21 职场文书
计算机网络专业自荐书
2014/06/09 职场文书
分公司总经理岗位职责
2014/07/30 职场文书
2015年销售员工作总结范文
2015/04/07 职场文书
CSS3 制作的图片滚动效果
2021/04/14 HTML / CSS
golang 比较浮点数的大小方式
2021/05/02 Golang