Python并發(fā)編程之IO模型
五種IO模型
為了更好地了解IO模型,我們需要事先回顧下:同步、異步、阻塞、非阻塞
- 同步(synchronous) IO
- 異步(asynchronous) IO
- 阻塞(blocking) IO
- 非阻塞(non-blocking)IO
五種I/O模型包括:阻塞I/O、非阻塞I/O、信號驅(qū)動I/O(不常用)、I/O多路轉(zhuǎn)接、異步I/O。其中,前四個被稱為同步I/O。
上五個模型的阻塞程度由低到高為:阻塞I/O > 非阻塞I/O > 多路轉(zhuǎn)接I/O > 信號驅(qū)動I/O > 異步I/O,因此他們的效率是由低到高的。
1、阻塞I/O模型
在linux中,默認(rèn)情況下所有的socket都是blocking,除非特別指定,幾乎所有的I/O接口 ( 包括socket接口 ) 都是阻塞型的。
如果所面臨的可能同時出現(xiàn)的上千甚至上萬次的客戶端請求,“線程池”或“連接池”或許可以緩解部分壓力,但是不能解決所有問題。總之,多線程模型可以方便高效的解決小規(guī)模的服務(wù)請求,但面對大規(guī)模的服務(wù)請求,多線程模型也會遇到瓶頸,可以用非阻塞接口來嘗試解決這個問題。
2、非阻塞I/O模型
在非阻塞式I/O中,用戶進(jìn)程其實(shí)是需要不斷的主動詢問kernel數(shù)據(jù)準(zhǔn)備好了沒有。但是非阻塞I/O模型絕不被推薦。
非阻塞,不等待。比如創(chuàng)建socket對某個地址進(jìn)行connect、獲取接收數(shù)據(jù)recv時默認(rèn)都會等待(連接成功或接收到數(shù)據(jù)),才執(zhí)行后續(xù)操作。
如果設(shè)置setblocking(False),以上兩個過程就不再等待,但是會報BlockingIOError的錯誤,只要捕獲即可。
異步,通知,執(zhí)行完成之后自動執(zhí)行回調(diào)函數(shù)或自動執(zhí)行某些操作(通知)。比如做爬蟲中向某個地址baidu。com發(fā)送請求,當(dāng)請求執(zhí)行完成之后自執(zhí)行回調(diào)函數(shù)。
3、多路復(fù)用I/O模型(事件驅(qū)動)
基于事件循環(huán)的異步非阻塞框架:如Twisted框架,scrapy框架(單線程完成并發(fā))。
檢測多個socket是否已經(jīng)發(fā)生變化(是否已經(jīng)連接成功/是否已經(jīng)獲取數(shù)據(jù))(可讀/可寫)IO多路復(fù)用作用?
操作系統(tǒng)檢測socket是否發(fā)生變化,有三種模式:
- select:最多1024個socket;循環(huán)去檢測。
- poll:不限制監(jiān)聽socket個數(shù);循環(huán)去檢測(水平觸發(fā))。
- epoll:不限制監(jiān)聽socket個數(shù);回調(diào)方式(邊緣觸發(fā))。
Python模塊:
- select.select
- select.epoll
基于IO多路復(fù)用+socket非阻塞,實(shí)現(xiàn)并發(fā)請求(一個線程100個請求)
import socket
# 創(chuàng)建socket
client = socket.socket()
# 將原來阻塞的位置變成非阻塞(報錯)
client.setblocking(False)
# 百度創(chuàng)建連接: 阻塞
try:
# 執(zhí)行了但報錯了
client.connect(('www.baidu.com',80))
except BlockingIOError as e:
pass
# 檢測到已經(jīng)連接成功
# 問百度我要什么?
client.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
# 我等著接收百度給我的回復(fù)
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'))selectors模塊
#服務(wù)端
from socket import *
import selectors
sel=selectors.DefaultSelector()
def accept(server_fileobj,mask):
conn,addr=server_fileobj.accept()
sel.register(conn,selectors.EVENT_READ,read)
def read(conn,mask):
try:
data=conn.recv(1024)
if not data:
print('closing',conn)
sel.unregister(conn)
conn.close()
return
conn.send(data.upper()+b'_SB')
except Exception:
print('closing', conn)
sel.unregister(conn)
conn.close()
server_fileobj=socket(AF_INET,SOCK_STREAM)
server_fileobj.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
server_fileobj.bind(('127.0.0.1',8088))
server_fileobj.listen(5)
server_fileobj.setblocking(False) #設(shè)置socket的接口為非阻塞
sel.register(server_fileobj,selectors.EVENT_READ,accept) #相當(dāng)于網(wǎng)select的讀列表里append了一個文件句柄
#server_fileobj,并且綁定了一個回調(diào)函數(shù)accept
while True:
events=sel.select() #檢測所有的fileobj,是否有完成wait data的
for sel_obj,mask in events:
callback=sel_obj.data #callback=accpet
callback(sel_obj.fileobj,mask) #accpet(server_fileobj,1)
#客戶端
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8088))
while True:
msg=input('>>: ')
if not msg:continue
c.send(msg.encode('utf-8'))
data=c.recv(1024)
print(data.decode('utf-8'))4、異步I/O
asyncio是Python 3.4版本引入的標(biāo)準(zhǔn)庫,直接內(nèi)置了對異步IO的支持。
asyncio的編程模型就是一個消息循環(huán)。我們從asyncio模塊中直接獲取一個EventLoop的引用,然后把需要執(zhí)行的協(xié)程扔到EventLoop中執(zhí)行,就實(shí)現(xiàn)了異步IO。
用asyncio實(shí)現(xiàn)Hello world代碼如下:
import asyncio
@asyncio.coroutine
def hello():
print("Hello world!")
# 異步調(diào)用asyncio.sleep(1):
r = yield from asyncio.sleep(1)
print("Hello again!")
# 獲取EventLoop:
loop = asyncio.get_event_loop()
# 執(zhí)行coroutine
loop.run_until_complete(hello())
loop.close()@asyncio.coroutine把一個generator標(biāo)記為coroutine類型,然后,我們就把這個coroutine扔到EventLoop中執(zhí)行。
hello()會首先打印出Hello world!,然后,yield from語法可以讓我們方便地調(diào)用另一個generator。由于asyncio.sleep()也是一個coroutine,所以線程不會等待asyncio.sleep(),而是直接中斷并執(zhí)行下一個消息循環(huán)。當(dāng)asyncio.sleep()返回時,線程就可以從yield from拿到返回值(此處是None),然后接著執(zhí)行下一行語句。
把asyncio.sleep(1)看成是一個耗時1秒的IO操作,在此期間,主線程并未等待,而是去執(zhí)行EventLoop中其他可以執(zhí)行的coroutine了,因此可以實(shí)現(xiàn)并發(fā)執(zhí)行。
我們用Task封裝兩個coroutine試試:
import threading
import asyncio
@asyncio.coroutine
def hello():
print('Hello world! (%s)' % threading.currentThread())
yield from asyncio.sleep(1)
print('Hello again! (%s)' % threading.currentThread())
loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()觀察執(zhí)行過程:
Hello world! (<_MainThread(MainThread, started 140735195337472)>) Hello world! (<_MainThread(MainThread, started 140735195337472)>) (暫停約1秒) Hello again! (<_MainThread(MainThread, started 140735195337472)>) Hello again! (<_MainThread(MainThread, started 140735195337472)>)
由打印的當(dāng)前線程名稱可以看出,兩個coroutine是由同一個線程并發(fā)執(zhí)行的。
如果把asyncio.sleep()換成真正的IO操作,則多個coroutine就可以由一個線程并發(fā)執(zhí)行。
我們用asyncio的異步網(wǎng)絡(luò)連接來獲取sina、sohu和163的網(wǎng)站首頁:
import asyncio
@asyncio.coroutine
def wget(host):
print('wget %s...' % host)
connect = asyncio.open_connection(host, 80)
reader, writer = yield from connect
header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
writer.write(header.encode('utf-8'))
yield from writer.drain()
while True:
line = yield from reader.readline()
if line == b'\r\n':
break
print('%s header > %s' % (host, line.decode('utf-8').rstrip()))
# Ignore the body, close the socket
writer.close()
loop = asyncio.get_event_loop()
tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()執(zhí)行結(jié)果如下:
wget www.sohu.com... wget www.sina.com.cn... wget www.163.com... (等待一段時間) (打印出sohu的header) www.sohu.com header > HTTP/1.1 200 OK www.sohu.com header > Content-Type: text/html ... (打印出sina的header) www.sina.com.cn header > HTTP/1.1 200 OK www.sina.com.cn header > Date: Wed, 20 May 2015 04:56:33 GMT ... (打印出163的header) www.163.com header > HTTP/1.0 302 Moved Temporarily www.163.com header > Server: Cdn Cache Server V2.0 ...
可見3個連接由一個線程通過coroutine并發(fā)完成。
async/await
用asyncio提供的@asyncio.coroutine可以把一個generator標(biāo)記為coroutine類型,然后在coroutine內(nèi)部用yield from調(diào)用另一個coroutine實(shí)現(xiàn)異步操作。
為了簡化并更好地標(biāo)識異步IO,從Python 3.5開始引入了新的語法async和await,可以讓coroutine的代碼更簡潔易讀。
請注意,async和await是針對coroutine的新語法,要使用新的語法,只需要做兩步簡單的替換:
- 把
@asyncio.coroutine替換為async; - 把
yield from替換為await。
讓我們對比一下上一節(jié)的代碼:
@asyncio.coroutine
def hello():
print("Hello world!")
r = yield from asyncio.sleep(1)
print("Hello again!")用新語法重新編寫如下:
async def hello():
print("Hello world!")
r = await asyncio.sleep(1)
print("Hello again!")剩下的代碼保持不變。
小結(jié)
asyncio提供了完善的異步IO支持;
異步操作需要在coroutine中通過yield from完成;
多個coroutine可以封裝成一組Task然后并發(fā)執(zhí)行。
到此這篇關(guān)于Python并發(fā)編程之IO模型的文章就介紹到這了。希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
python神經(jīng)網(wǎng)絡(luò)Keras?GhostNet模型的實(shí)現(xiàn)
這篇文章主要為大家介紹了python神經(jīng)網(wǎng)絡(luò)Keras?GhostNet模型的復(fù)現(xiàn)詳解,2022-05-05
Python 用turtle實(shí)現(xiàn)用正方形畫圓的例子
今天小編就為大家分享一篇Python 用turtle實(shí)現(xiàn)用正方形畫圓的例子,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11
Python腳本簡單實(shí)現(xiàn)打開默認(rèn)瀏覽器登錄人人和打開QQ的方法
這篇文章主要介紹了Python腳本簡單實(shí)現(xiàn)打開默認(rèn)瀏覽器登錄人人和打開QQ的方法,涉及Python針對瀏覽器及應(yīng)用程序的相關(guān)操作技巧,代碼非常簡單實(shí)用,需要的朋友可以參考下2016-04-04
python利用pymysql和openpyxl實(shí)現(xiàn)操作MySQL數(shù)據(jù)庫并插入數(shù)據(jù)
這篇文章主要為大家詳細(xì)介紹了如何使用Python連接MySQL數(shù)據(jù)庫,并從Excel文件中讀取數(shù)據(jù),將其插入到MySQL數(shù)據(jù)庫中,有需要的小伙伴可以參考一下2023-10-10
Django事務(wù)transaction的使用以及多個裝飾器問題
這篇文章主要介紹了Django事務(wù)transaction的使用以及多個裝飾器問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08
使用NumPy讀取MNIST數(shù)據(jù)的實(shí)現(xiàn)代碼示例
這篇文章主要介紹了使用NumPy讀取MNIST數(shù)據(jù)的實(shí)現(xiàn)代碼示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11

