Python:通过一个小案例深入理解IO多路复用
通過(guò)一個(gè)小案例深入理解IO多路復(fù)用
假如我們現(xiàn)在有這樣一個(gè)普通的需求,寫一個(gè)簡(jiǎn)單的爬蟲來(lái)爬取?;ňW(wǎng)的主頁(yè)
import requests
import timestart = time.time()url = 'http://www.xiaohuar.com/'
result = requests.get(url).textprint(result)
print(time.time()-start)
這樣子是顯然沒(méi)啥問(wèn)題的,總共耗時(shí)約為6秒
?
但是有沒(méi)有辦法更進(jìn)一步優(yōu)化呢,這里如果需要優(yōu)化我們首先需要知道一個(gè)知識(shí)點(diǎn)
就是requests這個(gè)模塊它底層其實(shí)是封裝了urllib2和urllib3的,而這兩個(gè)模塊底層其實(shí)就是socket
如果需要優(yōu)化,從requests是實(shí)現(xiàn)不了的,那么能不能從socket來(lái)呢
如果從socket,又該如何優(yōu)化呢?
?
首先我們得知道socket到底做了什么,
import socketclient = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
url = 'www.xiaohuar.com/'
client.connect((url, 80))
client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format('/',80).encode('utf8'))
data = b''
while 1:d = client.recv(1024)if d:data +=delse:breakprint(data)
這里的代碼就是上面那個(gè)requests版本的代碼的底層
在這一坨代碼中,有幾個(gè)點(diǎn)需要注意
connect和recv,這兩個(gè)方法都是阻塞io,也就是說(shuō),如果連接不到或者接受不到消息的話,程序就會(huì)一直等,等到預(yù)期的效果為止。
這就是阻塞
?
阻塞有個(gè)很大的弊端,那就是cpu無(wú)法得到充分利用,因?yàn)榈却臅r(shí)間里,cpu是空閑的,而我們又沒(méi)有執(zhí)行其他的操作,那么這段時(shí)間我們能不能充分利用起來(lái)呢
答案是肯定的,socket提供了一個(gè)非阻塞的辦法
client.setblocking(False)
直接運(yùn)行試試效果
BlockingIOError: [WinError 10035] 無(wú)法立即完成一個(gè)非阻止性套接字操作。
結(jié)果是拋出了這個(gè)異常,這是因?yàn)楫?dāng)變?yōu)榉亲枞麜r(shí)候,連接?;ňW(wǎng)的url的時(shí)候,三次握手還沒(méi)建立完成,我們就去執(zhí)行下一步了
try:client.connect((url, 80))
except BlockingIOError as e:
#處理其他事情pass
那么我們可以這樣改,抓到這個(gè)異常但是不處理,這樣子,我們就能在except后面加入其他的代碼了,也就是說(shuō)cpu發(fā)個(gè)請(qǐng)求就不管了,然后去執(zhí)行后面的代碼,這樣效率就提高了。
再運(yùn)行一次。
OSError: [WinError 10057] 由于套接字沒(méi)有連接并且(當(dāng)使用一個(gè) sendto 調(diào)用發(fā)送數(shù)據(jù)報(bào)套接字時(shí))沒(méi)有提供地址,發(fā)送或接收數(shù)據(jù)的請(qǐng)求沒(méi)有被接受。
又拋出了一個(gè)異常,和上面的原理差不多,因?yàn)槭欠亲枞J?/p>
最終代碼如下
import socketclient = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.setblocking(False)
url = 'www.xiaohuar.com'
try:client.connect((url, 80))
except BlockingIOError as e:passwhile 1:try:client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format('/',80).encode('utf8'))breakexcept Exception as e:passdata = b''
while 1:try:d = client.recv(1024)except Exception as e:continueif d:data += delse:breakprint(data)
這樣子雖然有一段時(shí)間更充分利用了cpu 但是代碼很亂,很麻煩,其次雖然是非阻塞,但是有兩個(gè)地方只是把之前的阻塞的時(shí)間花費(fèi)了在循環(huán)上,那么有沒(méi)有更好的辦法呢?
?
這里就要引入IO多路復(fù)用的概念了
IO復(fù)用就是通過(guò)一種機(jī)制,一個(gè)進(jìn)程可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(讀或者寫),都能夠通知程序來(lái)進(jìn)行相應(yīng)的讀寫操作,但是select,poll和epoll都是同步io,也就是說(shuō)這個(gè)讀寫過(guò)程是阻塞的,而異步io則無(wú)需自己進(jìn)行讀寫,異步io的實(shí)現(xiàn)會(huì)負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶內(nèi)存。
?
select在windows,OS X, 或者linux都能用,但是select最大監(jiān)視數(shù)量只能為1024
而poll的話其他幾乎與select一樣,只是突破了最大限制
而epoll就與前面這兩個(gè)都不一樣了,它底層使用了紅黑樹的數(shù)據(jù)結(jié)構(gòu),epoll使用一個(gè)文件描述符來(lái)管理多個(gè)文件描述符,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個(gè)事件表之中,這樣在用戶空間和內(nèi)核空間的copy只需一次。
而poll和select都是才用輪詢的方式,所以效率差就在這里體現(xiàn)出來(lái)了
?
最終代碼 異步IO
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
import socketselector = DefaultSelector()class Fetcher():def send_msg(self, key):selector.unregister(key.fd)self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format('/', 80).encode('utf8'))selector.register(self.client.fileno(), EVENT_READ, self.recv)def recv(self, key):d = self.client.recv(1024)if d:self.data += delse:selector.unregister(key.fd)print(self.data.decode('utf8'))def get_url(self, url):self.data = b''try:self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)self.client.connect((url, 80))except Exception as e:# 加入另外的邏輯passselector.register(self.client.fileno(), EVENT_WRITE, self.send_msg)def loop_forever():while 1:ready = selector.select()for key, mask in ready:call_back = key.datacall_back(key)if __name__ == '__main__':fet = Fetcher()fet.get_url('www.xiaohuar.com')loop_forever()
?
轉(zhuǎn)載于:https://www.cnblogs.com/Miracle-boy/p/10004684.html
總結(jié)
以上是生活随笔為你收集整理的Python:通过一个小案例深入理解IO多路复用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 什么食物能止咳化痰?
- 下一篇: 如何创建systemd定时任务