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获取网页上图片下载地址的方法
Mar 11 Python
用Python写冒泡排序代码
Apr 12 Python
使用python绘制常用的图表
Aug 27 Python
django.db.utils.ProgrammingError: (1146, u“Table‘’ doesn’t exist”)问题的解决
Jul 13 Python
python抖音表白程序源代码
Apr 07 Python
python中的TCP(传输控制协议)用法实例分析
Nov 15 Python
使用python写一个自动浏览文章的脚本实例
Dec 05 Python
tensorflow之tf.record实现存浮点数数组
Feb 17 Python
python Shapely使用指南详解
Feb 18 Python
keras中的loss、optimizer、metrics用法
Jun 15 Python
使用Python获取字典键对应值的方法
Apr 26 Python
Python使用plt.boxplot()函数绘制箱图、常用方法以及含义详解
Aug 14 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的explode和implode的使用说明
2011/07/17 PHP
成为好程序员必须避免的5个坏习惯
2014/07/04 PHP
php操作xml入门之cdata区段
2015/01/23 PHP
PHP Warning: Module 'modulename' already loaded in问题解决办法
2015/03/16 PHP
php比较两个字符串长度的方法
2015/07/13 PHP
简单的pgsql pdo php操作类实现代码
2016/08/25 PHP
Yii CFileCache 获取不到值的原因分析
2017/02/08 PHP
php中时间函数date及常用的时间计算
2017/05/12 PHP
利用javascript移动div层-javascript 拖动层
2009/03/22 Javascript
使用jQuery简化Ajax开发 Ajax开发入门
2009/10/14 Javascript
如何确保JavaScript的执行顺序 之实战篇
2011/03/03 Javascript
多种方法实现load加载完成后把图片一次性显示出来
2014/02/19 Javascript
JS中的form.submit()不能提交表单的错误原因
2014/10/08 Javascript
推荐25个超炫的jQuery网格插件
2014/11/28 Javascript
动态加载js、css的简单实现代码
2016/05/26 Javascript
对于input 框限定输入值为浮点型的js代码
2017/09/25 Javascript
Angular设置别名alias的方法
2018/11/08 Javascript
jQuery实现合并表格单元格中相同行操作示例
2019/01/28 jQuery
JavaScript观察者模式原理与用法实例详解
2020/03/10 Javascript
vue.js实现h5机器人聊天(测试版)
2020/07/16 Javascript
对于Python的Django框架使用的一些实用建议
2015/04/03 Python
python动态参数用法实例分析
2015/05/25 Python
对Python实现简单的API接口实例讲解
2018/12/10 Python
Python matplotlib画图与中文设置操作实例分析
2019/04/23 Python
pycharm第三方库安装失败的问题及解决经验分享
2020/05/09 Python
Python-openCV开运算实例
2020/07/05 Python
pandas使用函数批量处理数据(map、apply、applymap)
2020/11/27 Python
详解Python爬虫爬取博客园问题列表所有的问题
2021/01/18 Python
HTML5的结构和语义(1):前言
2008/10/17 HTML / CSS
德国高性价比网上药店:medpex
2017/07/09 全球购物
美国宠物美容和宠物用品购物网站:Cherrybrook
2018/12/07 全球购物
安德玛加拿大官网:Under Armour加拿大
2019/10/02 全球购物
软件工程专业推荐信
2013/10/28 职场文书
工作失职检讨书范文
2014/01/16 职场文书
征用土地赔偿协议书
2014/09/26 职场文书
关于PostgreSQL JSONB的匹配和交集问题
2021/09/14 PostgreSQL