異步IO
CPU的速度遠(yuǎn)遠(yuǎn)快于磁盤、網(wǎng)絡(luò)等IO荆忍。在一個(gè)線程中格带,CPU執(zhí)行代碼的速度極快,然而刹枉,一旦遇到IO操作叽唱,如讀寫文件、發(fā)送網(wǎng)絡(luò)數(shù)據(jù)時(shí)微宝,就需要等待IO操作完成棺亭,才能繼續(xù)進(jìn)行下一步操作。這種情況稱為同步IO蟋软。
在IO操作的過程中镶摘,當(dāng)前線程被掛起嗽桩,而其他需要CPU執(zhí)行的代碼就無法被當(dāng)前線程執(zhí)行了。
因?yàn)橐粋€(gè)IO操作就阻塞了當(dāng)前線程凄敢,導(dǎo)致其他代碼無法執(zhí)行碌冶,所以我們必須使用多線程或者多進(jìn)程來并發(fā)執(zhí)行代碼,為多個(gè)用戶服務(wù)贡未。每個(gè)用戶都會(huì)分配一個(gè)線程,如果遇到IO導(dǎo)致線程被掛起蒙袍,其他用戶的線程不受影響俊卤。
多線程和多進(jìn)程的模型雖然解決了并發(fā)問題,但是系統(tǒng)不能無上限地增加線程害幅。由于系統(tǒng)切換線程的開銷也很大消恍,所以,一旦線程數(shù)量過多以现,CPU的時(shí)間就花在線程切換上了狠怨,真正運(yùn)行代碼的時(shí)間就少了,結(jié)果導(dǎo)致性能嚴(yán)重下降邑遏。
由于我們要解決的問題是CPU高速執(zhí)行能力和IO設(shè)備的龜速嚴(yán)重不匹配佣赖,多線程和多進(jìn)程只是解決這一問題的一種方法。
另一種解決IO問題的方法是異步IO记盒。當(dāng)代碼需要執(zhí)行一個(gè)耗時(shí)的IO操作時(shí)憎蛤,它只發(fā)出IO指令,并不等待IO結(jié)果纪吮,然后就去執(zhí)行其他代碼了俩檬。一段時(shí)間后,當(dāng)IO返回結(jié)果時(shí)碾盟,再通知CPU進(jìn)行處理棚辽。
可以想象如果按普通順序?qū)懗龅拇a實(shí)際上是沒法完成異步IO的,異步IO模型需要一個(gè)消息循環(huán)冰肴,在消息循環(huán)中屈藐,主線程不斷地重復(fù)“讀取消息-處理消息”這一過程。
在“發(fā)出IO請(qǐng)求”到收到“IO完成”的這段時(shí)間里熙尉,同步IO模型下估盘,主線程只能掛起,但異步IO模型下骡尽,主線程并沒有休息遣妥,而是在消息循環(huán)中繼續(xù)處理其他消息。這樣攀细,在異步IO模型下箫踩,一個(gè)線程就可以同時(shí)處理多個(gè)IO請(qǐng)求爱态,并且沒有切換線程的操作。對(duì)于大多數(shù)IO密集型的應(yīng)用程序境钟,使用異步IO將大大提升系統(tǒng)的多任務(wù)處理能力锦担。
協(xié)程
協(xié)程,又稱微線程慨削,纖程洞渔。英文名Coroutine。
子程序缚态,或者稱為函數(shù)磁椒,在所有語言中都是層級(jí)調(diào)用,比如A調(diào)用B玫芦,B在執(zhí)行過程中又調(diào)用了C浆熔,C執(zhí)行完畢返回,B執(zhí)行完畢返回桥帆,最后是A執(zhí)行完畢医增。
所以子程序調(diào)用是通過棧實(shí)現(xiàn)的,一個(gè)線程就是執(zhí)行一個(gè)子程序老虫。
子程序調(diào)用總是一個(gè)入口叶骨,一次返回,調(diào)用順序是明確的祈匙。而協(xié)程的調(diào)用和子程序不同邓萨。
協(xié)程看上去也是子程序,但執(zhí)行過程中菊卷,在子程序內(nèi)部可中斷缔恳,然后轉(zhuǎn)而執(zhí)行別的子程序,在適當(dāng)?shù)臅r(shí)候再返回來接著執(zhí)行洁闰。
注意歉甚,在一個(gè)子程序中中斷,去執(zhí)行其他子程序扑眉,不是函數(shù)調(diào)用纸泄,有點(diǎn)類似CPU的中斷。比如子程序A腰素、B聘裁,但是在A中是沒有調(diào)用B的,所以協(xié)程的調(diào)用比函數(shù)調(diào)用理解起來要難一些弓千。
看起來A衡便、B的執(zhí)行有點(diǎn)像多線程,但協(xié)程的特點(diǎn)在于是一個(gè)線程執(zhí)行,那和多線程比镣陕,協(xié)程有何優(yōu)勢(shì)谴餐?
最大的優(yōu)勢(shì)就是協(xié)程極高的執(zhí)行效率。因?yàn)樽映绦蚯袚Q不是線程切換呆抑,而是由程序自身控制岂嗓,因此,沒有線程切換的開銷鹊碍,和多線程比变抽,線程數(shù)量越多泻轰,協(xié)程的性能優(yōu)勢(shì)就越明顯贵白。
第二大優(yōu)勢(shì)就是不需要多線程的鎖機(jī)制谷暮,因?yàn)橹挥幸粋€(gè)線程蛀柴,也不存在同時(shí)寫變量沖突吞滞,在協(xié)程中控制共享資源不加鎖掰读,只需要判斷狀態(tài)就好了亡容,所以執(zhí)行效率比多線程高很多品洛。
因?yàn)閰f(xié)程是一個(gè)線程執(zhí)行树姨,那怎么利用多核CPU呢?最簡(jiǎn)單的方法是多進(jìn)程+協(xié)程桥状,既充分利用多核帽揪,又充分發(fā)揮協(xié)程的高效率,可獲得極高的性能辅斟。
Python對(duì)協(xié)程的支持是通過generator實(shí)現(xiàn)的转晰。
生產(chǎn)者生產(chǎn)消息后,直接通過yield跳轉(zhuǎn)到消費(fèi)者開始執(zhí)行士飒,待消費(fèi)者執(zhí)行完畢后查邢,切換回生產(chǎn)者繼續(xù)生產(chǎn),效率極高:
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
c = consumer()
produce(c)
執(zhí)行結(jié)果:
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK
注意到consumer函數(shù)是一個(gè)generator酵幕,把一個(gè)consumer傳入produce后:
- 首先調(diào)用c.send(None)啟動(dòng)生成器扰藕;
- 然后,一旦生產(chǎn)了東西芳撒,通過c.send(n)切換到consumer執(zhí)行邓深;
- consumer通過yield拿到消息,處理笔刹,又通過yield把結(jié)果傳回芥备;
- produce拿到consumer處理的結(jié)果,繼續(xù)生產(chǎn)下一條消息舌菜;
- produce決定不生產(chǎn)了萌壳,通過c.close()關(guān)閉consumer,整個(gè)過程結(jié)束。
整個(gè)流程無鎖讶凉,由一個(gè)線程執(zhí)行染乌,produce和consumer協(xié)作完成任務(wù),所以稱為“協(xié)程”懂讯,而非線程的搶占式多任務(wù)荷憋。
asyncio
Python實(shí)現(xiàn)異步IO非常簡(jiǎn)單,asyncio是Python 3.4版本引入的標(biāo)準(zhǔn)庫褐望,直接內(nèi)置了對(duì)異步IO的支持勒庄。
asyncio
的編程模型就是一個(gè)消息循環(huán)。我們從asyncio
模塊中直接獲取一個(gè)EventLoop
的引用瘫里,然后把需要執(zhí)行的協(xié)程扔到EventLoop
中執(zhí)行实蔽,就實(shí)現(xiàn)了異步IO。
用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...
(等待一段時(shí)間)
(打印出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
...
@asyncio.coroutine
把一個(gè)generator標(biāo)記為coroutine類型局装,然后,我們就把這個(gè)coroutine
扔到EventLoop
中執(zhí)行劳殖。
yield from
語法可以讓我們方便地調(diào)用另一個(gè)generator铐尚。所以線程不會(huì)等待IO操作,而是直接中斷并執(zhí)行下一個(gè)消息循環(huán)哆姻。當(dāng)yield from
返回時(shí)宣增,線程就可以從yield from
拿到返回值,然后接著執(zhí)行下一行語句矛缨。
在此期間爹脾,主線程并未等待,而是去執(zhí)行EventLoop
中其他可以執(zhí)行的coroutine
了箕昭,因此我們用Task封裝的三個(gè)coroutine
可以實(shí)現(xiàn)由同一個(gè)線程并發(fā)執(zhí)行灵妨。
async/await
為了簡(jiǎn)化并更好地標(biāo)識(shí)異步IO,從Python 3.5開始引入了新的語法async和await落竹,可以讓coroutine的代碼更簡(jiǎn)潔易讀泌霍。
使用新語法,只需要做兩步簡(jiǎn)單的替換:
- 把
@asyncio.coroutine
替換為async
筋量; - 把
yield from
替換為await
烹吵。
用新語法重新編寫上一節(jié)的代碼如下:
import asyncio
async def wget(host):
print('wget %s...' % host)
connect = asyncio.open_connection(host, 80)
reader, writer = await connect
header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
writer.write(header.encode('utf-8'))
await writer.drain()
while True:
line = await 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()
剩下的代碼保持不變。
aiohttp
asyncio
實(shí)現(xiàn)了TCP桨武、UDP肋拔、SSL等協(xié)議,aiohttp
則是基于asyncio
實(shí)現(xiàn)的HTTP框架呀酸。
安裝aiohttp:
pip install aiohttp
然后編寫一個(gè)HTTP服務(wù)器凉蜂,分別處理以下URL:
-
/
- 首頁返回b'<h1>Index</h1>'
; -
/hello/{name}
- 根據(jù)URL參數(shù)返回文本hello, %s!
。
代碼如下:
import asyncio
from aiohttp import web
async def index(request):
await asyncio.sleep(0.5)
return web.Response(body=b'<h1>Index</h1>')
async def hello(request):
await asyncio.sleep(0.5)
text = '<h1>hello, %s!</h1>' % request.match_info['name']
return web.Response(body=text.encode('utf-8'))
async def init(loop):
app = web.Application(loop=loop)
app.router.add_route('GET', '/', index)
app.router.add_route('GET', '/hello/{name}', hello)
srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000)
print('Server started at http://127.0.0.1:8000...')
return srv
loop = asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()
注意aiohttp
的初始化函數(shù)init()
也是一個(gè)coroutine
窿吩,loop.create_server()
則利用asyncio
創(chuàng)建TCP服務(wù)茎杂。
祝愿早日戰(zhàn)勝新冠病毒,武漢加油纫雁!中國加油煌往!