目录
一、协程
进程是资源分配的最小单位,线程是CPU调度的最小单位。
对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程。
1.1 介绍
优点如下:
1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2. 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点如下:
1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
总结协程特点:
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
- 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
1.2 Greenlet模块
用法介绍:
g1=gevent.spawn(func)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的
g2=gevent.spawn(func2)
g1.join() #等待g1结束
g2.join() #等待g2结束
#或者上述两步合作一步:gevent.joinall([g1,g2])
g1.value#拿到func1的返回值
栗子:
from greenlet import greenlet
def eat():
print('eating start')
g2.switch()
print('eating end')
g2.switch()
def play():
print('playing start')
g1.switch()
print('playing end')
g1 =greenlet(eat)
g2 =greenlet(play)
g1.switch()
eating start
playing start
eating end
playing end
1.3 gevent模块
协程要用的模块
直接上栗子:
from gevent import monkey;monkey.patch_all() 猴子补丁一定写到最上面,它会把之后导入包的大部分的阻塞式系统调用(sleep、IO操作)记录下来变成协程式
from gevent import monkey;monkey.patch_all() 猴子补丁一定写到最上面,它会把之后导入包的大部分的阻塞式系统调用(sleep、IO操作)记录下来变成协程式
import time
import gevent
def eat():
print('eating start')
time.sleep(1)
print('eating end')
def play():
print('playing start')
time.sleep(1)
print('playing end')
g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)
g1.join()
g2.join()
同步和异步的区别:
from gevent import monkey;monkey.patch_all()
import time
import gevent
def task(n):
time.sleep(1)
print(n)
def sync(): #同步
for i in range(10):
task(i)
def async1(): # 异步
g_lst = []
for i in range(10):
g = gevent.spawn(task,i)
g_lst.append(g)
gevent.joinall(g_lst) # for g in g_lst:g.join()
sync()
async1()
爬虫小栗子:
from gevent import monkey;monkey.patch_all()
import gevent
from urllib.request import urlopen
def get_url(url):
response = urlopen(url)
content = response.read().decode('utf-8')
return len(content)
g1 = gevent.spawn(get_url, 'http://www.baidu.com')
g2 = gevent.spawn(get_url, 'http://www.sogou.com')
g3 = gevent.spawn(get_url, 'http://www.taobao.com')
g4 = gevent.spawn(get_url, 'http://www.hao123.com')
g5 = gevent.spawn(get_url, 'http://www.cnblogs.com')
gevent.joinall([g1,g2,g3,g4,g5])
print(g1.value)
print(g2.value)
print(g3.value)
print(g4.value)
print(g5.value)
二、IO模型介绍
本文讨论的背景是Linux环境下的network IO。本文最重要的参考文献是Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models ”,Stevens在这节中详细说明了各种IO的特点和区别,如果英文够好的话,推荐直接阅读。Stevens的文风是有名的深入浅出,所以不用担心看不懂。本文中的流程图也是截取自参考文献。
为了更好地了解IO模型,我们需要事先回顾下:同步、异步、阻塞、非阻塞
同步 提交一个任务之后要等待这个任务执行完毕 异步 只管提交任务,不等待这个任务执行完毕就可以做其他事情 阻塞 recv recvfrom accept 非阻塞
Stevens在文章中一共比较了五种IO Model:
* blocking IO 阻塞IO
* nonblocking IO 非阻塞IO
* IO multiplexing IO多路复用
* signal driven IO 信号驱动IO
* asynchronous IO 异步IO
由signal driven IO(信号驱动IO)在实际中并不常用,所以主要介绍其余四种IO Model。
2.1 阻塞IO(blocking IO)
默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
所以,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。
对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。
2.2 非阻塞IO(non-blocking IO)
可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。于是用户就可以在本次到下次再发起read询问的时间间隔内做其他事情,或者直接再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存(这一阶段仍然是阻塞的),然后返回。
所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。
上个非阻塞IO实例:(使用socket模块的server端)
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.setblocking(False) 设置socket的接口为非阻塞
sk.listen()
conn_l = []
del_conn = []
while True:
try:
conn,addr = sk.accept() #不阻塞,但是没人连我会报错
print('建立连接了:',addr)
conn_l.append(conn)
except BlockingIOError:
for con in conn_l:
try:
msg = con.recv(1024) # 非阻塞,如果没有数据就报错
if msg == b'':
del_conn.append(con)
continue
print(msg)
con.send(b'byebye')
except BlockingIOError:pass
for con in del_conn:
con.close()
conn_l.remove(con)
del_conn.clear()
# while True : 10000 500 501
代码图解:
但是也难掩其缺点:
#1. 循环调用recv()将大幅度推高CPU占用率;低配主机下极容易出现卡机情况 #2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
此外,在这个方案中recv()更多的是起到检测“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成“作用的接口,例如select()多路复用模式,可以一次检测多个连接是否活跃。
2.3 多路复用IO(IO multiplexing)
流程如图:
中文版:
强调:
1. 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
2. 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
结论: select的优势在于可以处理多个连接,不适用于单个连接
IO多路复用用到的常用模块:
# select机制 使用环境:Windows linux 操作系统轮询每一个被监听的项,看是否有读操作
# poll机制 使用环境: linux 操作系统轮询每一个被监听的项,看是否有读操作,监听的对象比select机制多
# select机制和poll机制随着监听项的增多,导致效率降低
# epoll机制 使用环境: linux
栗子:多路复用IO 的socket端(用select模块实现):
import select
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8000))
sk.setblocking(False)
sk.listen()
read_lst = [sk]
while True: # [sk,conn]
r_lst,w_lst,x_lst = select.select(read_lst,[],[])
for i in r_lst:
if i is sk:
conn,addr = i.accept()
read_lst.append(conn)
else:
ret = i.recv(1024)
if ret == b'':
i.close()
read_lst.remove(i)
continue
print(ret)
i.send(b'goodbye!')
client端:
import time
import socket
import threading
def func():
sk = socket.socket()
sk.connect(('127.0.0.1',8000))
sk.send(b'hello')
time.sleep(3)
print(sk.recv(1024))
sk.close()
for i in range(20):
threading.Thread(target=func).start()
该模型的优点:
相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。
该模型的缺点:
1、#首先select()接口并不是“事件驱动”的最好选择。因为当探测的连接很多时,select()接口本身需要消耗大量时间去轮询各个连接。 2、#很多操作系统提供了更为高效的接口,如linux提供了epoll 3、#如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异,所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。 4、#其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。
2.4 异步IO(Asynchronous I/O)
示例图:
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
目前Python代码无法实现,c语言可以实现,或者用C语言写的Python
2.5 IO模型比较分析
经过上面的介绍,会发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。