协程
Hello,大家好,我是景天。今天我们来一起探讨一下Python的异步神技—协程。
协程也叫纤程: 协程是线程的一种实现方式.
指的是一条线程能够在多任务之间来回切换的一种实现.
对于CPU、操作系统来说,协程并不存在.
任务之间的切换会花费时间.
目前电脑配置一般线程开到200会阻塞卡顿.
1.协程是什么?
协程又称微线程,纤程。
它是比线程更小的执行单元,因为它自带CPU上下文。这样只要在合适的时机,我们可以把一个协程切换到另一个协程当中。
只要这个过程保存或恢复CPU上下文,那么程序就可以运行。
通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,
(注意不是通过调用函数的方式来实现),并且切换的次数以及什么时候再切换到原来的函数由开发者确定。
2.协程和线程有什么不同
那么这个过程看起来和线程差不多。其实不然, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。
操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。
所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。
3.协程的实现
协程帮助你记住哪个任务执行到哪个位置上了,并且实现安全的切换
一个任务一旦阻塞卡顿,立刻切换到另一个任务继续执行,保证线程总是忙碌的,更加充分的利用CPU,抢占更多的时间片
一个线程可以由多个协程来实现,协程之间不会产生数据安全问题
协程模块
# greenlet gevent的底层,协程,切换的模块
# gevent 直接用的,gevent能提供更全面的功能
进程是资源分配的最小单位
线程是程序调度的最小单位
协程是线程实现的具体方式
在进程一定的情况下,开辟多个线程,
在线程一定的情况下,创建多个协程,
以便提高更大的并行并发
协程是基于单线程实现的异步并发结构,可以让线程执行任务遇到阻塞时立马切换到其他任务,当前任务就绪后,接着执行。保证线程总是忙碌的,更加充分的利用CPU,抢占更多的时间片
(1) 用协程改写生产者消费者模型
def producer(): for i in range(1000): yield i def consumer(gen): for i in range(10): print( next(gen) ) gen = producer() consumer(gen) print("<==========>") consumer(gen) print("<==========>") consumer(gen)
(2) greenlet 协程的早期版本
from greenlet import greenlet import time switch 可以切换任务,但是需要手动切换 def eat(): print("eat1") g2.switch() time.sleep(3) print("eat2") def play(): print("play1") time.sleep(3) print("play2") g1.switch() #创建协程对象,把要执行的任务函数塞进去 g1 = greenlet(eat) g2 = greenlet(play) g1.switch()
协程帮助切换任务,回头的时候还能记住代码执行状态
g1.switch()开始执行eat这个任务
在eat任务里面,遇到g2.switch(),开始执行play任务
在play任务里面,遇到g1.switch()开始回到eat任务上次切换的地方,继续往下执行
greenlet需要手动切换,很不方便
(3) 升级到gevent版本
需要安装,导入显示没有时,Windows直接安装
linux安装
pip3 install gevent
自动进行任务上的切换,但是不能识别阻塞
import gevent def eat(): print("eat1") gevent.sleep(3) # time.sleep(3) print("eat2") def play(): print("play1") gevent.sleep(3) # time.sleep(3) print("play2") #利用gevent.spawn创建协程对象g1 g1 = gevent.spawn(eat) #参数是任务函数,任务函数的参数,可以跟多个参数 #利用gevent.spawn创建协程对象g2 g2 = gevent.spawn(play) #如果不加join, 主线程直接结束任务,不会默认等待协程任务. #阻塞,必须等待g1任务完成之后在放行 g1.join() #阻塞,必须等待g2任务完成之后在放行 g2.join() print("主线程执行结束 .... ")
如果只是创建协程对象,任务有阻塞,协程不会运行,代码已结束。gevent()不识别阻塞
主线程默认不会等待当前线程中协程中的任务
所以为了让协程中的代码执行完,再执行主线程中的代码,需要join,必须等待协程中任务执行完在往下执行,但是只用时间阻塞,任务没有切换
使用gevent.sleep(),遇到就切换
(4) 协程的终极版本
引入猴子补丁,可以实现所有的阻塞全部识别
from gevent import monkey;monkey.patch_all() import time import gevent def eat(): print("eat1") time.sleep(3) print("eat2") def play(): print("play1") time.sleep(3) print("play2") # 利用gevent.spawn创建协程对象g1 g1 = gevent.spawn(eat) # 利用gevent.spawn创建协程对象g2 g2 = gevent.spawn(play) # 如果不加join, 主线程直接结束任务,不会默认等待协程任务. # 阻塞,必须等待g1任务完成之后在放行 g1.join() # 阻塞,必须等待g2任务完成之后在放行 g2.join() print(" 主线程执行结束 ... ")
利用monkey补丁,识别所有阻塞
导入monkey之后
执行monkey.patch_all()
把要导入的模块放入下面
也可以在导入monkey的时候加分号,放在一行写。然后下面导入模块 这样,各种阻塞都能识别,切换
导入monkey模块,必须放在最上面,其他模块如果放在其上面,导致报错
导入monkey放顶格,不再报错
gevent.spwn()创建协程到时候,可以带参数
可以使用gevent.joinall([g1,g2…])一次性join。不用一个个单独join
4.协程例子
# (1) spawn(函数,参数1,参数2,参数 .... ) 启动协程 # (2) join 阻塞,直到某个协程在任务执行完毕之后在放行 # (3) joinall 等待所有协程任务执行完毕之后放行; g1.join() g2.join() <=> gevent.joinall( [g1,g2..] ) # (4) value 获取协程任务中的返回值 g1.value g2.value 在线程池中,还可以通过submit对象,调用add_done_callback()来获取任务中的返回值 猴子补丁一定要在gevent之前引进来。最好把猴子补丁放在最前面 from gevent import monkey ; monkey.patch_all() import gevent import time import requests def eat(): print("eat1 开始吃 ... ") time.sleep(1) print("eat2 继续吃 ... ") return "吃完了" def play(): print("play1 开始玩 ... ") time.sleep(1) print("play2 继续玩 ... ") return "玩完了" # 创建协程对象g1 g1 = gevent.spawn(eat) # 创建协程对象g2 g2 = gevent.spawn(play) # 等待所有协程任务执行完毕之后放行 gevent.joinall( [g1,g2] ) print("主线程执行结束 ... ") # 获取协程任务中的返回值 print(g1.value) print(g2.value)
带有参数的协程,并获取协程返回值
(1) 利用协程爬取数据
HTTP 状态码
200 ok
400 bad request
404 not found
import requests #需要安装requests response = requests.get("http://www.baidu.com") #抓取网站一定得加协议,不然抓不了 #返回的是个类 # print(response ,type(response) ) # 获取状态码 print(response.status_code) # 获取网页中的字符编码 res = response.apparent_encoding print(res) # utf-8 # 设置编码集,防止乱码 response.encoding = res # 获取网页内容 res = response.text print(res)
一般要先获取网站原编码,然后根据网站原编码设置编码编码集,不然无脑设置成utf-8的话,原编码不是utf-8也会乱码
url_lst = [ "http://www.baidu.com", "http://www.jd.com/", "http://www.taobao.com/", "http://www.amazon.cn/", "http://www.pinduoduo.com/", "http://www.4399.com/", "http://www.baidu.com", "http://www.jd.com/", "http://www.taobao.com/", "http://www.amazon.cn/", "http://www.pinduoduo.com/", "http://www.4399.com/", "http://www.baidu.com", "http://www.jd.com/", "http://www.taobao.com/", "http://www.amazon.cn/", "http://www.pinduoduo.com/", "http://www.4399.com/", "http://www.baidu.com", "http://www.jd.com/", "http://www.taobao.com/", "http://www.amazon.cn/", "http://www.pinduoduo.com/", "http://www.4399.com/", "http://www.baidu.com", "http://www.jd.com/", "http://www.taobao.com/", "http://www.amazon.cn/", "http://www.pinduoduo.com/", "http://www.4399.com/", "http://www.baidu.com", "http://www.jd.com/", "http://www.taobao.com/", "http://www.amazon.cn/", "http://www.pinduoduo.com/", "http://www.4399.com/", "http://www.baidu.com", "http://www.jd.com/", "http://www.taobao.com/", "http://www.amazon.cn/", "http://www.pinduoduo.com/", "http://www.4399.com/", "http://www.baidu.com", "http://www.jd.com/", "http://www.taobao.com/", "http://www.amazon.cn/", "http://www.pinduoduo.com/", "http://www.4399.com/", "http://www.baidu.com", "http://www.jd.com/", "http://www.taobao.com/", "http://www.amazon.cn/", "http://www.pinduoduo.com/", "http://www.4399.com/", "http://www.baidu.com", "http://www.jd.com/", "http://www.taobao.com/", "http://www.amazon.cn/", "http://www.pinduoduo.com/", "http://www.4399.com/", "http://www.baidu.com", "http://www.jd.com/", "http://www.taobao.com/", "http://www.amazon.cn/", "http://www.pinduoduo.com/", "http://www.4399.com/" ] def get_url(url): response = requests.get(url) if response.status_code == 200: # print(response.text) pass
1. 正常爬取
startime = time.time() for i in url_lst: get_url(i) endtime = time.time() print(endtime-startime)
正常单线程爬取用时7.09112286567688秒
2.用协程的方法爬取数据
lst = [] startime = time.time() #每个页面设置一个协程,多少个链接,创建多少个协程。当有网站阻塞,协程立马切到其他网站,等该网站就绪,协程再切回来 for i in url_lst: g = gevent.spawn(get_url , i) lst.append(g) gevent.joinall( lst ) endtime = time.time() print("主线程执行结束 ... 时间{}".format(endtime-startime))
用多协程爬取总用时0.8327083587646484。1秒左右
多协程确实快好多