python中的协程深入理解
作者:且听风吟 发布时间:2021-05-27 21:47:36
先介绍下什么是协程:
协程,又称微线程,纤程,英文名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的执行过程如下图所示:
调用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,在开始执行之后:
调用next(c)启动生成器;
进入do_work,这是一个递归调用,其内部将url传递给consumer,由consumer来发出请求,获取到html信息,返回给produce,
produce解析html,获取url数据,继续生产url,
当所有的url都在history_urls中,也就是说我们已经爬取了所有的url地址,结束递归调用
调用c.close(),关闭consumer,整个过程结束。
可以看到,我们的整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
来源:https://www.lylinux.net/article/2019/1/13/54.html
猜你喜欢
- (一).确认删除用法: 1. BtnDel.Attributes.Add("onclick","return
- 前两天拉取公司前端代码修改,发现在开发者工具的sources选项里边,居然没有列出来我要调试的js脚本,后来观察了一下,脚本是动态在页面里引
- 接着上篇的内容,这里实现一个交易记录链,废话不多说,先看图:跟之前的逻辑类似,但也有少许不同,这里多了一个payloadhash,以及对pa
- 一个小需求---实现车牌识别。目前有两个想法1. 调云在线的接口或者使用SDK做开发(配置环境和编译第三方库很麻烦,当然使用python可以
- 在运行Go语言程序之前,先要将其编译成二进制的可执行文件我们通常在编辑器编写完源码后使用go build或go run命令对GO语言程序进行
- 前言哈喽,大家好,我是asong。最近没事在看八股文,总结了几道常考的切片八股文,以问答的方式总结出来,希望对正在面试的你们有用~本文题目不
- OpenCV+python3将视频分解成图片,供大家参考,具体内容如下我们在工作或学习时,偶尔需要将视频分解成图片,只取其中一段的图片就行了
- 什么是闭包?简单说,闭包就是根据不同的配置信息得到不同的结果。再来看看专业的解释:闭包(Closure)是词法闭包(Lexical Clos
- 目录常规思路更快捷的方法使用方法对数据库的表进行重命名可以使用以下原生sql:RENAME TABLE old_table TO new_t
- 在线文本去重复工具第一种方法:<textarea id="list" class="toolarea&q
- Character 字符串:数据类型描述存储char(n)固定长度的字符串。最多 8,000 个字符。nvarchar(n)可变长度的字符串
- 过滤非法的SQL字符的函数代码:'*************************************************
- 本文实例讲述了java实现的连接oracle mysql数据库功能。分享给大家供大家参考,具体如下:package com.nuo.test
- 本文实例讲述了Python实现的建造者模式。分享给大家供大家参考,具体如下:#!/usr/bin/python# -*- coding:ut
- 经测试可用的发送邮件代码:import smtplibfrom email.mime.text import MIMEText# 第三方 S
- 本文记录了mysql 5.7.18绿色版下载安装的详细过程1、先下载mysq5.7.18绿色版,然后解压出来,放在一个目录下,我的解压mys
- 这是我以前发表在经典论坛的帖子,现在转贴回来。仿淘宝网站的导航效果。此方法有几个优点:根据字数自适应项目长度不同的项目使用不同的颜色来区分无
- Python 类Python中的类提供了面向对象编程的所有基本功能:类的继承机制允许多个基类,派生类可以覆盖基类中的任何方法,方法中可以调用
- 0x00:事先说明你已经攻陷了对方主机且获得了最高权限。对方的本地防火墙会丢弃所有的外来数据包。这个后门不会仅绑定在某一个端口上。这段代码很
- 前言最近在学习vue框架的基本原理,看了一些技术博客以及一些对vue源码的简单实现,对数据代理、数据劫持、模板解析、变异数组方法、双向绑定有