一、协程简介
协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。
协程优势:1.无需线程上下文切换的消耗 2.无需加锁,协程就是一种单线程模式,串行不用锁 3.高并发,高扩展性 4.协程能保留上次调用的状态,重新进入时候继续上次退出地方。
我的理解
协程简单点说,就是一个进程可以启动多个线程,每个线程中都可以开启很多个协程,完成不同事件的处理,这就是协程高并发的特点。因为协程用到的不多,我们就不讲解底层的代码了,协程封装在greenlet模块中,我们在使用时候调用这个模块即可。
举一个例子更好的理解协程。我们在一个代码中启动两个协程,分别完成两个任务,假设第一个任务需要下载文件(需要花5s时间),我们都知道python实际上都是单线程模式,不管干嘛都要一个一个执行,但是可以通过切换的方式来使得程序显得是在多路同时处理。那么我们开启的两个协程,正常情况是,先执行协程1,花费5s。在执行协程2,假设花费3s。一共就花费了8s。我们发现这是一种效率极低的方式。如果有上万个协程执行,可想而知需要花费多久的时间。
那么我们可不可以在执行协程1时候,遇到下载(也就是I/O操作)就转而执行协程2,等协程2执行完,再回来执行协程1。这样就会花费最长的一个协程执行的时间,也就是5s。下面举一个简单的代码来验证一下:
from greenlet import greenlet #导入封装好的协程
def test1():
print(12)
gr2.switch() #我们这里模拟I/O操作(上面说的下载5s),遇到这种就跳转到gr2协程执行
print(34)
gr2.switch()
def test2():
print(56)
gr1.switch()
print(78)
gr1=greenlet(test1)
gr2=greenlet(test2) #启动两个协程
gr1.switch() #手动切换协程,执行gr1
根据上面的代码我相信大家都会看出执行结果,这就是协程之间的切换,一旦特别多的协程启动,就会大大缩短时间,那么如果有很多协程启动,我们在进行手动切换就会容易出错,所以看一下自动切换协程的方法,自动切换协程封装在gevent中:
import gevent #自动切换协程
def foo():
print('Running in foo')
gevent.sleep(2) #模拟IO操作,耗费2s时间,跳转到下一个协程
print('Running in foo again')
def bar():
print('Explicit context switch to bar')
gevent.sleep(1) #遇到I/O操作,跳转到下一个协程
print('Explicit context switch to bar again')
def zoo():
print('Explicit context switch to zoo')
gevent.sleep(0) #0秒也是进行了I/O操作
print('Explicit context switch to zoo again')
gevent.joinall ([
gevent.spawn(foo), #开启3个协程,每个协程调用不同的函数
gevent.spawn(bar),
gevent.spawn(zoo),
])
看一下结果,我们分析一下:
先开启协程foo,打印Running in foo,遇到I/O操作2s跳转到协程bar,打印Explicit context switch to bar,遇到I/O操作1s,跳转到协程zoo,打印Explicit context switch to zoo,遇到I/O操作0s跳转到协程foo,发现2s没到,切换到协程bar,发现1s没到,切换到协程zoo,0s到了,打印Explicit context switch to zoo again,切换协程foo,2s没到,切换协程bar,1s到了打印Explicit context switch to bar again,最后切换回foo,打印Running in foo again。这样就完成了自动切换,本来执行需要3s,现在只需要2s。
二、协程应用
1.协程之爬虫
我们已经了解了协程在进行高并发时候如何工作,下面我们说一个很经典的协程可以使用的场景,爬虫我不知道大家了不了解,过一阶段我会写一些爬虫的文章,包括爬虫底层代码和scrapy框架等。在进行爬虫时候,我们要获取很多的数据,如果每一次只等着上一次的内容爬取完在进行下次爬取效率很低,我们可以使用协程的方式:
假设启动一个进程,创建100个线程,每个线程中启动1000个协程,每个协程爬取10个网站,那么我们同时就可以爬取100万个网站信息了。那么如何利用协程进行数据的爬取呢?我这里写一个最基础的爬虫代码,如果不理解就简单查一下语句用法即可:
from urllib import request #爬取网页的模块
import gevent
#gevent默认情况检测不到urllib 所以不会进行协程切换,爬取三个网页时间和串行爬取三个网页时间一样大概4秒
#那么如何让gevent知道urllib进行了IO操作呢:打一个补丁monkey就可以了
from gevent import monkey
monkey.patch_all() #把当前程序的所有IO操作单独做上标记,这样gevent就可以进行切换了,时间减小为2秒左右。
def f(url): #传入参数:网址
print('GET:%s'%url)
resp=request.urlopen(url)
data=resp.read() #获取网页信息
print('%d bytes received from %s' % (len(data), url)) #打印网页数据大小和网址
gevent.joinall ([
gevent.spawn(f,'https://www.python.org/'), #带参数(url)方式开启协程
gevent.spawn(f,'https://www.yahoo.com/'),
gevent.spawn(f,'https://github.com/'),
])
看一下结果:
访问3个网页数据的时间由4s缩短到2s,可想而知如果获取上万个网站信息,效率会大大提高。
2.协程之ftp
ftp我就不多说了,不懂的可以看看我另一篇文章:
https://mp.youkuaiyun.com/mdeditor/81502058
那么协程怎么实现socket通信呢?我们先写一个server端:
import socket,time,gevent,sys
from gevent import monkey
monkey.patch_all() #补丁
def server(port):
s=socket.socket()
s.bind(('0.0.0.0',port)) #端口号
s.listen(500)
while True:
cli,addr=s.accept() #接受到的数据返回给cli,addr
#过来一个客户端链接(就是说有一个客户端连接),就交给一个协程去处理,调用函数handle_request
gevent.spawn(handle_request,cli)
def handle_request(conn):
try:
while True:
data=conn.recv(1024) #接收客户端发来的数据
print('recv:',data)
conn.send(data) #把接收来的数据发回给客户端
if not data:
conn.shutdown(socket.SHUT_WR)
except Exception as ex:
print(ex)
finally:
conn.close()
if __name__ == '__main__':
server(8001)
写一下client:
#我们打开5个客户端, 通过协程的方式就实现了多线程的方式,可以让多个客户端连接服务器
import socket
HOST='localhost'
PORT=8001
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((HOST,PORT)) #连接服务器
while True:
msg=bytes(input('>>:'),encoding= 'utf-8') #用户输入发送的数据
s.sendall(msg) #发数据给客户端
data=s.recv(1024) #收数据
print('Received',repr(data)) #repr格式化输出
s.close()
看一下结果:
我们发现服务器可以接收到5个客户端发来的数据,111和11111111都是客户端1发来的以此类推,我们看一下服务器是否给客户端返回了数据,看一下客户端3
这样就实现了协程的socket通信,如果有成千上万个客户端想要连接服务器时候,使用这种架构也是不错的。
协程就说到这里了,进程、线程、协程这三个还是跟重要的。