协程与IO多路复用
两者解决的问题相似,都是在解决一个线程中的高并发问题
IO多路复用:
I:input(输入的意思)
O:output(输出的意思)
先用一个例子来引出IO多路复用
以目前学的知识解决爬虫时的并发问题,你一定会这样做方法一就是直接调用封装好的request模块方便简单
还有方法二就是多线程,这些都没有错.那么有没有想过在单线程的基础上去优化一下呢?
import socket import requests # #################### 解决并发:单线程 #################### # 方式一 key_list = ['alex','db','sb'] for item in key_list: ret = requests.get('https://www.baidu.com/s?wd=%s' %item)
# #################### 解决并发:多线程 ####################
import threading
key_list = ['alex','db','sb']
for item in key_list:
t = threading.Thread(target=get_data,args=(item,))
t.start()
我们来分析一下在服务端发送网络请求的时候到底在那些地方出现了阻塞,能不能给优化一下 # 方式二 def get_data(key): # 方式二 client = socket.socket() # 百度创建连接: 阻塞 client.connect(('www.baidu.com',80)) # 问百度我要什么? client.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n') # 我等着接收百度给我的回复 chunk_list = [] while True: chunk = client.recv(8096) if not chunk: break chunk_list.append(chunk) body = b''.join(chunk_list) print(body.decode('utf-8')) key_list = ['alex','db','sb'] for item in key_list: get_data(item)
通过分析我们需要解决的就是在连接是的阻塞和在发送的时候的阻塞
setblocking
在socket模块中有这样一个功能就是setblocking 来看用法
import socket client = socket.socket() client.setblocking(False) # 将原来阻塞的位置变成非阻塞(报错) # 百度创建连接: 阻塞 try: client.connect(('www.baidu.com',80)) # 执行了但报错了 except BlockingIOError as e: pass # 检测到已经连接成功 # 问百度我要什么? client.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n') # 我等着接收百度给我的回复 chunk_list = [] while True: chunk = client.recv(8096) # 将原来阻塞的位置变成非阻塞(报错) if not chunk: break chunk_list.append(chunk) body = b''.join(chunk_list) print(body.decode('utf-8'))
我们这种解决方案就是在原来阻塞的地方加上个标志让其报错然后在捕获这个错误,将原来阻塞的位置变成非阻塞
两个概念:
非阻塞:不等待
异步:执行完某个任务后自动调用我给他的函数
爬取多个网站的并发解决如果现在要求要爬取不同网站上的一些内容也只能用单线程这样的问题其实不只是你一个人遇到,有些人在纠结了解决并发问题你直接开多线程不就行了或者土豪一点开一个多进程就好了,干嘛要这么费劲的在一个线程了这么纠结呢我要说的就是在实际的开发中我们要尽量的节省资源,因为只有这样你的东西才能被人们长期的使用,所以,言归正传在Python有个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.sogou.com',80)) except BlockingIOError as e: pass client3 = socket.socket() client3.setblocking(False) # 百度创建连接: 非阻塞 try: client3.connect(('www.oldboyedu.com',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)
#所有的都与setblocking用法差不多就是在这里不太一样,我们来分析这里面的一些参数rlist与socket_list是
一对他们是用来放爬取成功后的内容,也就是阻塞中的rcev,elist与conn_list是一对他们存放的就是连接的响应
就好比connect连接的过程,这样又一个功能区帮我们监测这些有阻塞的地方,我们的线程就有跟多的时间去做跟多的事
这样是不是在单线程中提高了我们的线程工作效率呢? 仔细品味一下
]
# wlist中表示已经连接成功的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=fdf HTTP/1.0\r\nhost:www.sogou.com\r\n\r\n') else: sk.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.oldboyedu.com\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.decode('utf-8')) print('------------>',body) sk.close() socket_list.remove(sk) if not socket_list: break
拓展:
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: # 发生变换的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) # print(body.decode('utf-8')) sk.func(body) sk.sock.close() self.socket_list.remove(sk) if not self.socket_list: break def baidu_repsonse(body): print('百度下载结果:',body) def sogou_repsonse(body): print('搜狗下载结果:', body) def oldboyedu_repsonse(body): print('老男孩下载结果:', body) t1 = Nb() t1.add('www.baidu.com',baidu_repsonse) t1.add('www.sogou.com',sogou_repsonse) t1.add('www.oldboyedu.com',oldboyedu_repsonse) t1.run()
协程:
是微线程,对一个线程进行分片使得线程在代码块之间进行来回切换执行,而不是在原来逐行执行
(协程是由程序员造出来的一个不是真实存在的东西)那个例子来说
协程首先要引入greenlet模块
import greenlet def f1(): print(11) gr2.switch() print(22) gr2.switch() def f2(): print(33) gr1.switch() print(44) # 协程 gr1 gr1 = greenlet.greenlet(f1) # 协程 gr2 gr2 = greenlet.greenlet(f2) gr1.switch()
这里面switch选择谁就执行谁,所以是不是在程序员自己发明的,当然你也可以自己做一个属于自己的
这样做的过程中随之而来的问题也就来了,在IO操作中怎么去切换选择
这就要引入一个新的功能
from gevent import monkey monkey.patch_all() # 以后代码中遇到IO都会自动执行greenlet的switch进行切换 import requests import gevent def get_page1(url): ret = requests.get(url) print(url,ret.content) def get_page2(url): ret = requests.get(url) print(url,ret.content) def get_page3(url): ret = requests.get(url) print(url,ret.content) gevent.joinall([ gevent.spawn(get_page1, 'https://www.python.org/'), # 协程1 gevent.spawn(get_page2, 'https://www.yahoo.com/'), # 协程2 gevent.spawn(get_page3, 'https://github.com/'), # 协程3 ])
这样就解决了单线程的并发问题
拓展内容:手动实现协程:yield关键字生成器
def f1(): print(11) yield print(22) yield print(33) def f2(): print(55) yield print(66) yield print(77) v1 = f1() v2 = f2() next(v1) # v1.send(None) next(v2) # v1.send(None) next(v1) # v1.send(None) next(v2) # v1.send(None) next(v1) # v1.send(None) next(v2) # v1.send(None)
def f1(): print(11) x1 = yield 1 print(x1, 22) x2 = yield 2 print(33) def f2(): print(55) yield print(66) yield print(77) v1 = f1() v2 = f2() ret = v1.send(None) print(ret) r2 = v1.send(999) print(r2)