协程
协程,又称微线程,纤程。英文名Coroutine。
协程是啥?
首先我们得知道协程是啥?协程其实可以认为是比线程更小的执行单元。为啥说他是一个执行单元,因为他自带CPU上下文。这样只要在合适的时机,我们可以把一个协程切换到另一个协程。只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。
通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定
协程和线程差异
那么这个过程看起来比线程差不多。其实不然, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。
协程的问题
但是协程有一个问题,就是系统并不感知,所以操作系统不会帮你做切换。那么谁来帮你做切换?让需要执行的协程更多的获得CPU时间才是问题的关键。
实现协程不止一种方法,有利用yield、利用greenlet、利用gevent等。
首先是
利用yield
实现协程的切换,代码如下:
#-*- coding:utf-8 -*-
#协程的简单实现 python3.6
import time
def A():
while True:
print('----A----')
yield #将A变为一个生成器,此时A已经不算一个函数了
time.sleep(0.5)
def B(c):
print('----B----')
c.__next__() #利用next() 切换协程,将执行A中的代码块
time.sleep(0.5)
if __name__ == '__main__':
a = A() # a代表生成器 此语句并不执行A()中的输出语句
#第一次调用时必须先next()或send(None)
#否则会报错,send后之所以为None是因为这时候没有上一个yield。可以认为,next()等同于send(None)。
print(a.__next__())
B(a)
#yield 是一个类似 return 的关键字,迭代一次遇到yield时就返回yield后面的值。
#重点是:下一次迭代时,从上一次迭代遇到的yield后面的代码开始执行。
#简要理解:yield就是 return 返回一个值,并且记住这个返回的位置,下次迭代就从这个位置后开始。
输出结果为:
----A----
None
----B----
----A----
***Repl Closed***
接下来是
利用greenlet
进行协程的实现:
#-*- coding:utf-8 -*-
from greenlet import greenlet
import time
def test1():
while True:
print('----A----')
gr2.switch() #切换到gre2
time.sleep(0.5)
def test2():
while True:
print('----B----')
gr1.switch()
time.sleep(0.5)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch() #切换线程,执行gr1
输出结果为:
----A----
----B----
----A----
----B----
......
greenlet的实现较为简单,理解起来不算困难,需要提前利用pip下载greenlet。切换协程的方法是用switch方法进行切换。
值得一提的是yield、greenlet进行切换都是手动控制的,无法令系统自动控制。因此引入了gevent,可以自动检测出耗时操作,并且切换协程。
简单执行的代码如下:
#-*- coding:utf-8 -*-
import gevent
def f(n):
for i in range(n):
print(gevent.getcurrent(), i)
#gevent.sleep(1)
# 该语句是耗时操作,gevent会检测到并且切换线程的执行
g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()
执行结果是:
<Greenlet at 0x2e47f80: f(5)> 0
<Greenlet at 0x2e47f80: f(5)> 1
<Greenlet at 0x2e47f80: f(5)> 2
<Greenlet at 0x2e47f80: f(5)> 3
<Greenlet at 0x2e47f80: f(5)> 4
<Greenlet at 0x2eaf170: f(5)> 0
<Greenlet at 0x2eaf170: f(5)> 1
<Greenlet at 0x2eaf170: f(5)> 2
<Greenlet at 0x2eaf170: f(5)> 3
<Greenlet at 0x2eaf170: f(5)> 4
<Greenlet at 0x2eaf1c0: f(5)> 0
<Greenlet at 0x2eaf1c0: f(5)> 1
<Greenlet at 0x2eaf1c0: f(5)> 2
<Greenlet at 0x2eaf1c0: f(5)> 3
<Greenlet at 0x2eaf1c0: f(5)> 4
***Repl Closed***
可以看出是按顺序执行g1、g2、g3的,如果不将那一句耗时操作注释的话,结果是:
<Greenlet at 0x2ab7f80: f(5)> 0
<Greenlet at 0x2b1f170: f(5)> 0
<Greenlet at 0x2b1f1c0: f(5)> 0
<Greenlet at 0x2ab7f80: f(5)> 1
<Greenlet at 0x2b1f1c0: f(5)> 1
<Greenlet at 0x2b1f170: f(5)> 1
<Greenlet at 0x2ab7f80: f(5)> 2
<Greenlet at 0x2b1f170: f(5)> 2
<Greenlet at 0x2b1f1c0: f(5)> 2
<Greenlet at 0x2ab7f80: f(5)> 3
<Greenlet at 0x2b1f1c0: f(5)> 3
<Greenlet at 0x2b1f170: f(5)> 3
<Greenlet at 0x2ab7f80: f(5)> 4
<Greenlet at 0x2b1f170: f(5)> 4
<Greenlet at 0x2b1f1c0: f(5)> 4
***Repl Closed***
可以发现在遇到耗时操作之后,协程直接被切换了,也就是g1、g2、g3之间来回切换,这是一件很神奇的事!
之后我们便进入正题,开始写一下利用gevent实现的并发式单进程服务器。原理自然是利用spawn方法进行线程的切换。
思路必须要清晰,我们实现的并发式服务器自然是要避免阻塞的,可以思考下单进程服务器是在哪里受到阻塞的?在套接字执行accept的时候、以及接受客户端消息的时候。在gevent包的socket中,已经改变了正常的socket,所以可以自动检测出accept的耗时操作,我们要做的就是将接收消息的代码封装为函数,通过spawn方法进行协程的切换以及耗时操作的检测。
代码如下:
import sys
import time
import gevent
#利用协程实现并发服务器
from gevent import socket, monkey
#gevent将耗时操作进行了重写,故重新导入
monkey.patch_all()
#底层修改源代码 实现gevent相关功能
def handle_request(conn):
while True:
recvData = conn.recv(1024)# 耗时操作
if not recvData:
conn.close()
print("client was closed")
break
print("recv:", recvData)
coon.send(recvData)
def sever(port):
s = socket.socket()
s.bind(('', port))
s.listen(5)
while True:
cli, addr = s.accept() # 耗时操作
print("new client from [%s] "%str(addr))
# 遇到耗时操作自动切换协程
gevent.spawn(handle_request, cli)
if __name__ == '__main__':
sever(7788)
#协程的实现中只有一个箭头,负责切换协程,箭头指向哪里执行哪里,遇到耗时操作将进行切换
值得一提的是,要引入monkey模块。实质上是从底层上修改了源代码,使得代码符合gevent的要求。
最重要的便是在windows下其实是无法实现gevent的,但是我们可以通过下载车轮来进行测试,网站是:
要根据你的系统以及python版本进行下载哦
。下载之后便可以通过pip进行安装!