24 IO多路复用and异步非阻塞and协程
#先来记下概念性东西
IO多路复用作用:检测所有IO请求(主要是socket)是否已经发生变化(是否已经连接成功/是否已经获取数据)(可读/可写)
同步:按顺序执行
阻塞:等待
异步:执行完成之后自动执行回掉函数或自动执行某些操作(通知)
非阻塞:不等待
协程:本身是个没什么用的东西,一般跟IO操作一起使用。协程的作用就是进行分片,使得线程在代码快之间按照你的需求来切换
执行,而不是原来的逐行执行。
1、如果我们要用实现socket的并发请求。除了用多线程和多进程的。单线程也能实现:
1、使用基于事件循环的IO多路复用+非阻塞,也可以达到并发效果。分别访问三个网址,拿到返回(如果拿到后有后续操作那么就是异步了)。这里用到select模块
原始代码如下:
import socket import select client1 = socket.socket() client1.setblocking(False) try: client1.connect(('www.baidu.com', 80)) except BlockingIOError as e: pass client2 = socket.socket() client2.setblocking(False) try: client2.connect(('www.sougou.com', 80)) except BlockingIOError as e: pass client3 = socket.socket() client3.setblocking(False) try: client3.connect(('so.m.sm.cn', 80)) except BlockingIOError as e: pass socket_list = [client1, client2, client3] conn_list = [client1, client2, client3] while True: rlist,wlist,elist = select.select(socket_list, conn_list, [], 0.005) ''' 一共四个参数。 第一个参数。socket_list,检测服务端是否返回数据---可读 第二个参数。conn_list,检测其中的socket是否已经和服务端连接成功---可写 第三个参数。[],用来检测异常 第四个参数。每次检测时间间隔 ''' for sk in wlist: if sk == client1: sk.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n') elif sk == client2: sk.sendall(b'GET /web?query=alex HTTP/1.0\r\nhost:www.sogou.com\r\n\r\n') else: sk.sendall(b'GET /s?q=alex HTTP/1.0\r\nhost:so.m.sm.cn\r\n\r\n') conn_list.remove(sk) for sk in rlist: chunk_list = [] while True: try: chunk = sk.recv(8096) if not chunk: break chunk_list.append(chunk) except BlockingIOError as e: break body = b''.join(chunk_list) print('-------->', body) #print(body.decode('utf-8')) sk.close() socket_list.remove(sk) if not socket_list: break
封装版本如下:
# coding:utf-8 import socket import select class Req(object): def __init__(self, sk, func): self.sock = sk self.func = func def fileno(self): return self.sock.fileno() class NB(object): def __init__(self): self.conn_list = [] self.socket_list = [] def add(self, url, func): client = socket.socket() client.setblocking(False) # 非阻塞 try: client.connect((url, 80)) except BlockingIOError as e: pass obj = Req(client, func) self.conn_list.append(obj) self.socket_list.append(obj) def run(self): while True: rlist,wlist,elist = select.select(self.socket_list, self.conn_list, [], 0.005) # wlist中表示已经连接成功的req对象 for sk in wlist: # sk:发生变化的req对象 sk.sock.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n') self.conn_list.remove(sk) for sk in rlist: chunk_list = [] while True: try: chunk = sk.sock.recv(8096) if not chunk: break chunk_list.append(chunk) except BlockingIOError as e: break body = b''.join(chunk_list) sk.func(body) sk.sock.close() self.socket_list.remove(sk) if not self.socket_list: break def baidu_response(body): print('百度下载的结果:', body) def sogou_response(body): print('搜狗下载的结果:', body) def so_response(body): print('uc下载的结果:', body) t1 = NB() t1.add('www.baidu.com', baidu_response) t1.add('www.sogou.com', sogou_response) t1.add('so.m.sm.cn', so_response) t1.run()
注意点:
1、socket阻塞之后要捕获BlockingIOError异常。
2、对比同步阻塞。异步非阻塞请求发过去之后,继续发下一个请求,不会等待消耗时间。然后回来的时候排队。但是比起多线程还是差距挺大的。
2、协程:
单独协程用到greenlet模块
如下简单demo
import greenlet def f1(): print(11) # 1、打印这里 gr2.switch() print(22) # 3、打印这里 gr2.switch() def f2(): print(33) # 2、打印这里 gr1.switch() print(44) # 4、打印这里 gr1 = greenlet.greenlet(f1) # 协程gr1 gr2 = greenlet.greenlet(f2) # 协程gr2 gr1.switch()
所以说单独的协程没用。下面是配合IO切换来使用。这里用到了monkey模块
from gevent import monkey monkey.patch_all() # 下面代码中遇到IO都会自动执行greenlet的switch进行切换 import requests import gevent def get_page_1(url): ret = requests.get(url) print(url, ret.content) def get_page_2(url): ret = requests.get(url) print(url, ret.content) def get_page_3(url): ret = requests.get(url) print(url, ret.content) gevent.joinall([ gevent.spawn(get_page_1, 'https://www.python.org/'), # 协程1 gevent.spawn(get_page_2, 'https://www.yahoo.com/'), # 协程2 gevent.spawn(get_page_3, 'https://www.github.com/'), # 协程3 ])
这里就比单独来回访问三次要快得多。由上面的例子可得。协程+IO切换和基于事件循环的IO多路复用(有个Twisted框架)本质差不太多。