深入探索Python中的协程

文章目录

协程

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秒左右
多协程确实快好多

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值