Python 異步IO

異步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后:

  1. 首先調(diào)用c.send(None)啟動(dòng)生成器扰藕;
  2. 然后,一旦生產(chǎn)了東西芳撒,通過c.send(n)切換到consumer執(zhí)行邓深;
  3. consumer通過yield拿到消息,處理笔刹,又通過yield把結(jié)果傳回芥备;
  4. produce拿到consumer處理的結(jié)果,繼續(xù)生產(chǎn)下一條消息舌菜;
  5. 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)單的替換:

  1. @asyncio.coroutine替換為async筋量;
  2. 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)勝新冠病毒,武漢加油纫雁!中國加油煌往!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市轧邪,隨后出現(xiàn)的幾起案子刽脖,更是在濱河造成了極大的恐慌,老刑警劉巖忌愚,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件曲管,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡硕糊,警方通過查閱死者的電腦和手機(jī)院水,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來简十,“玉大人檬某,你說我怎么就攤上這事∩自叮” “怎么了橙喘?”我有些...
    開封第一講書人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵时鸵,是天一觀的道長胶逢。 經(jīng)常有香客問我,道長饰潜,這世上最難降的妖魔是什么初坠? 我笑而不...
    開封第一講書人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮彭雾,結(jié)果婚禮上碟刺,老公的妹妹穿的比我還像新娘。我一直安慰自己薯酝,他們只是感情好半沽,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吴菠,像睡著了一般者填。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上做葵,一...
    開封第一講書人閱讀 52,475評(píng)論 1 312
  • 那天占哟,我揣著相機(jī)與錄音,去河邊找鬼。 笑死榨乎,一個(gè)胖子當(dāng)著我的面吹牛怎燥,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蜜暑,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼铐姚,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了肛捍?” 一聲冷哼從身側(cè)響起谦屑,我...
    開封第一講書人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎篇梭,沒想到半個(gè)月后氢橙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡恬偷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年悍手,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片袍患。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡坦康,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诡延,到底是詐尸還是另有隱情滞欠,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布肆良,位于F島的核電站筛璧,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏惹恃。R本人自食惡果不足惜夭谤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望巫糙。 院中可真熱鬧朗儒,春花似錦、人聲如沸参淹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽浙值。三九已至恳不,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間亥鸠,已是汗流浹背妆够。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來泰國打工识啦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人神妹。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓颓哮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親鸵荠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子冕茅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 異步IO CPU的速度遠(yuǎn)遠(yuǎn)快于磁盤、網(wǎng)絡(luò)等IO蛹找。在一個(gè)線程中姨伤,CPU執(zhí)行代碼的速度極快,然而庸疾,一旦遇到IO操作乍楚,如...
    時(shí)間之友閱讀 6,501評(píng)論 4 17
  • 一、協(xié)程 執(zhí)行結(jié)果 注意到consumer函數(shù)是一個(gè)generator届慈,把一個(gè)consumer傳入produce后...
    _YZG_閱讀 337評(píng)論 0 0
  • 原文:http://www.reibang.com/p/4e048726b613 引言 隨著node.js的盛行徒溪,...
    jacke121閱讀 2,110評(píng)論 1 3
  • 我是一個(gè)喜歡表達(dá)的人,從大學(xué)到現(xiàn)在一直都是金顿。大學(xué)時(shí)候臊泌,我會(huì)寫無數(shù)的說說和空間日志,來表達(dá)我自己對(duì)很多事情的...
    琛琛_0622閱讀 132評(píng)論 0 1
  • linux 防火墻開啟和關(guān)閉 重啟后生效 開啟: chkconfig iptables on 關(guān)閉: chkcon...
    iHelin閱讀 203評(píng)論 0 0