python網絡編程基礎(連載)07 協(xié)程

gitbook鏈接:用python帶你進入AI中的深度學習技術領域https://www.gitbook.com/book/scrappyzhang/python_to_deeplearn/details

github鏈接:https://github.com/ScrappyZhang/python_web_Crawler_DA_ML_DL

6 協(xié)程

6.1 協(xié)程

協(xié)程柠衅,又稱微線程,纖程籍琳。英文名Coroutine菲宴。

協(xié)程的概念很早就提出來了,但直到最近幾年才在某些語言(如Lua)中得到廣泛應用趋急。

子程序喝峦,或者稱為函數,在所有語言中都是層級調用呜达,比如A調用B谣蠢,B在執(zhí)行過程中又調用了C,C執(zhí)行完畢返回查近,B執(zhí)行完畢返回眉踱,最后是A執(zhí)行完畢。

所以子程序調用是通過棧實現的霜威,一個線程就是執(zhí)行一個子程序谈喳。

子程序調用總是一個入口,一次返回侥祭,調用順序是明確的叁执。而協(xié)程的調用和子程序不同茄厘。

協(xié)程看上去也是子程序,但執(zhí)行過程中谈宛,在子程序內部可中斷次哈,然后轉而執(zhí)行別的子程序,在適當的時候再返回來接著執(zhí)行吆录。

注意窑滞,在一個子程序中中斷,去執(zhí)行其他子程序恢筝,不是函數調用哀卫,有點類似CPU的中斷。比如子程序A撬槽、B:

def A():
    print('1')
    print('2')
    print('3')

def B():
    print('4')
    print('5')
    print('6')

正常情況下此改,會輸出123456 。假設由協(xié)程執(zhí)行侄柔,在執(zhí)行A的過程中共啃,可以隨時中斷,去執(zhí)行B暂题,B也可能在執(zhí)行過程中中斷再去執(zhí)行A移剪,結果可能是:

1
2
4
5
3
6

但是在A中是沒有調用B的,所以協(xié)程的調用比函數調用理解起來要難一些薪者。

看起來A纵苛、B的執(zhí)行有點像多線程,但協(xié)程的特點在于是一個線程執(zhí)行言津,那和多線程比攻人,協(xié)程有何優(yōu)勢?

最大的優(yōu)勢就是協(xié)程極高的執(zhí)行效率纺念。因為子程序切換不是線程切換贝椿,而是由程序自身控制,因此陷谱,沒有線程切換的開銷烙博,和多線程比,線程數量越多烟逊,協(xié)程的性能優(yōu)勢就越明顯渣窜。

第二大優(yōu)勢就是不需要多線程的鎖機制,因為只有一個線程宪躯,也不存在同時寫變量沖突乔宿,在協(xié)程中控制共享資源不加鎖,只需要判斷狀態(tài)就好了访雪,所以執(zhí)行效率比多線程高很多详瑞。

因為協(xié)程是一個線程執(zhí)行掂林,那怎么利用多核CPU呢?最簡單的方法是多進程+協(xié)程坝橡,既充分利用多核泻帮,又充分發(fā)揮協(xié)程的高效率,可獲得極高的性能计寇。

注:在實現多任務時, 線程切換從系統(tǒng)層面遠不止保存和恢復 CPU上下文這么簡單锣杂。 操作系統(tǒng)為了程序運行的高效性每個線程都有自己緩存Cache等等數據,操作系統(tǒng)還會幫你做這些數據的恢復操作番宁。 所以線程的切換非常耗性能元莫。但是協(xié)程的切換只是單純的操作CPU的上下文,所以一秒鐘切換個上百萬次系統(tǒng)都抗的住蝶押。

6.2 python通過生成器實現協(xié)程

Python對協(xié)程的支持是通過generator生成器實現的踱蠢。在generator生成器中,我們不但可以通過for循環(huán)來迭代棋电,還可以不斷調用next()函數獲取由yield語句返回的下一個值朽基。Python的yield不但可以返回一個值,它還可以接收調用者發(fā)出的參數离陶。

yield的作用

掛起當前函數,將yield后面的值當做返回給調用生成器的地方衅檀;能夠在喚醒生成器函數的時候招刨,回復代碼繼續(xù)緊接著從上次執(zhí)行的地方執(zhí)行(可以接受額外的參數)

'''net05_yield.py'''
import time


def sing():
    for i in range(5):
        print('正在唱歌呢 %d' % i)
        yield
        time.sleep(1)


def dance():
    for i in range(5):
        print('正在跳舞呢 %d' % i)
        yield
        time.sleep(1)

if __name__ == '__main__':
    s1 = sing() # 唱歌
    d1 = dance() # 跳舞
    i = 5
    while i > 0:
        next(s1) # next獲取由yield語句的協(xié)程切換
        next(d1)
        i -= 1

結果如下:

正在唱歌呢 0
正在跳舞呢 0
正在唱歌呢 1
正在跳舞呢 1
正在唱歌呢 2
正在跳舞呢 2
正在唱歌呢 3
正在跳舞呢 3
正在唱歌呢 4
正在跳舞呢 4

首先,我們應當注意到代碼中的sing和dance函數中的for循環(huán)是一個生成器哀军,這是python協(xié)程的前提沉眶。通過yield實現協(xié)程切換,next來調用完成各生成器的下一步動作杉适。整個過程在一個線程內完成谎倔,非常高效;不需要多線程的鎖猿推,不存在線程安全問題片习。

需要注意的是:在用yield來完成send參數傳遞時需要先執(zhí)行一次next,然后才可以send傳遞參數蹬叭∨河剑可以看例子:

在第一次喚醒生成器代碼時,我們使用next(f)秽五。在后續(xù)的協(xié)程切換中孽查,我們使用f.send(100)來講參數100傳遞給gen中的temp;通過value = f.send()將yield返回的值i賦給value坦喘。

'''net05_yield_variable.py'''
def gen():
    i = 0
    while i < 5:
        temp = yield i
        print('send過來的值為', temp)
        i += 1


f = gen()
# 在第一次喚醒生成器代碼的時候 必須使用next(f) -- 在生成器代碼第一次執(zhí)行的時候 沒有可以接收參數的功能
print('第一次傳遞過來的值為', next(f))

while True:
    try:
        # value = next(f)
        value = f.send(100)

    except Exception as e:
        print('結束')
        break
    else:
        print("傳遞過來元素的值是%d" % value)
    finally:
        pass

結果:

第一次傳遞過來的值為 0
send過來的值為 100
傳遞過來元素的值是1
send過來的值為 100
傳遞過來元素的值是2
send過來的值為 100
傳遞過來元素的值是3
send過來的值為 100
傳遞過來元素的值是4
send過來的值為 100
結束

6.3 協(xié)程——greenlet

為了更好使用協(xié)程來完成多任務盲再,python中的greenlet模塊對其協(xié)程進行了封裝西设,從而省去next等使得切換任務變的更加簡單。我們可以通過pip install greenlet安裝并使用它答朋。

它一般通過創(chuàng)建greenlet對象贷揽,并在相應的代碼塊里假如switch語句來實現不同函數間的切換。來繼續(xù)修改唱歌跳舞例子:

'''net05_greenlet.py'''
import time
from greenlet import greenlet  # 導入greenlet.greenlet


def sing():
    for i in range(5):
        print('正在唱歌呢 %d' % i)
        d1.switch()  # 切換到跳舞函數
        time.sleep(1)


def dance():
    for i in range(5):
        print('正在跳舞呢 %d' % i)
        s1.switch()  # 切換到唱歌函數
        time.sleep(1)


if __name__ == '__main__':
    s1 = greenlet(sing)  # 唱歌
    d1 = greenlet(dance)  # 跳舞
    s1.switch()  # 切換到唱歌函數

結果如下:

正在唱歌呢 0
正在跳舞呢 0
正在唱歌呢 1
正在跳舞呢 1
正在唱歌呢 2
正在跳舞呢 2
正在唱歌呢 3
正在跳舞呢 3
正在唱歌呢 4
正在跳舞呢 4

我們首先創(chuàng)建了兩個greenlet實例對象绿映,然后從主程序通過s1.switch()切換到sing函數進行唱歌模塊擒滑。在sing函數中我們又通過d1.switch()切換到跳舞函數模塊;在dance函數中通過s1.switch()切換到sing函數叉弦。這樣便實現了交替切換執(zhí)行丐一。就像我們分析的那樣,它確實簡化了next等操作淹冰,但是需要開發(fā)者手動設置switch來實現不同函數之間的切換库车。

6.4 協(xié)程——gevent

正如上一節(jié)所說,greenlet需要手動設置切換樱拴,并不友好柠衍,所以本節(jié)介紹一個更友好的協(xié)程模塊gevent。我們可能需要通過pip install gevent來安裝它晶乔。

gevent原理是當一個greenlet遇到IO(指的是input output 輸入輸出珍坊,比如網絡、文件操作等)操作時正罢,比如訪問網絡阵漏,就自動切換到其他的greenlet,等到IO操作完成翻具,再在適當的時候切換回來繼續(xù)執(zhí)行履怯。由于IO操作非常耗時,經常使程序處于等待狀態(tài)裆泳,有了gevent為我們自動切換協(xié)程叹洲,就保證總有greenlet在運行,而不是等待IO工禾。

gevent一般通過以下語句創(chuàng)建協(xié)程并執(zhí)行:

gevent.spawn(函數名运提,參數)

但是它創(chuàng)建的協(xié)程默認不自動切換,需要使用gevent包的monkey來進行破解切換闻葵,語句如下:

from gevent import monkey
monkey.patch_all()

我們繼續(xù)修改我們的唱歌跳舞實例糙捺,以gevent協(xié)程的方式來實現同時唱歌跳舞:

'''net05_gevent.py'''
import time
import gevent
# 默認協(xié)程不切換,需要使用monkey此語句來破解
from gevent import monkey

monkey.patch_all()


def sing():
    for i in range(5):
        print('正在唱歌呢 %d' % i)
        time.sleep(1)


def dance():
    for i in range(5):
        print('正在跳舞呢 %d' % i)
        time.sleep(1)


if __name__ == '__main__':
    g1 = gevent.spawn(sing)
    g2 = gevent.spawn(dance)
    g1.join()
    g2.join()

結果是一樣的笙隙,至此洪灯,我們分別通過多線程、多進程和協(xié)程三種方式實現了同時唱歌跳舞。

正在唱歌呢 0
正在跳舞呢 0
正在唱歌呢 1
正在跳舞呢 1
正在唱歌呢 2
正在跳舞呢 2
正在唱歌呢 3
正在跳舞呢 3
正在唱歌呢 4
正在跳舞呢 4

6.5 進程签钩、線程掏呼、協(xié)程區(qū)別

  • 進程是資源分配的單位
  • 線程是操作系統(tǒng)調度的單位
  • 進程切換需要的資源很最大,效率很低
  • 線程切換需要的資源一般铅檩,效率一般
  • 協(xié)程切換任務資源很小憎夷,效率高
  • 多進程、多線程根據cpu核數不一樣可能是并行的 也可能是并發(fā)的昧旨。協(xié)程的本質就是使用當前進程在不同的函數代碼中切換執(zhí)行拾给,可以理解為并行。 協(xié)程是一個用戶層面的概念兔沃,不同協(xié)程的模型實現可能是單線程 也可能是多線程蒋得。

6.7 協(xié)程實現網頁并發(fā)下載

需求實現:

通過gevent協(xié)程來同時下載百度、163乒疏、hao123的主頁html并保存到本地额衙。

完整源代碼:

'''net05_html_download.py'''
from gevent import monkey
import gevent
import urllib.request

monkey.patch_all()


def my_download(url):
    print('GET: %s' % url)
    resp = urllib.request.urlopen(url)
    data = resp.read()
    input_file = url.lstrip('http://www.').rstrip('.com/') + '.html'
    with open(input_file, 'wb') as html_in_file:
        html_in_file.write(data)
    print('%d bytes received from %s.' % (len(data), url))

# joinall 為阻塞主程序使得列表內所有協(xié)程完成
gevent.joinall([
    gevent.spawn(my_download, 'http://www.baidu.com/'),
    gevent.spawn(my_download, 'http://www.163.com/'),
    gevent.spawn(my_download, 'http://www.hao123.com/')
])

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市怕吴,隨后出現的幾起案子窍侧,更是在濱河造成了極大的恐慌,老刑警劉巖转绷,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伟件,死亡現場離奇詭異,居然都是意外死亡议经,警方通過查閱死者的電腦和手機锋爪,發(fā)現死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來爸业,“玉大人,你說我怎么就攤上這事亏镰〕犊酰” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵索抓,是天一觀的道長钧忽。 經常有香客問我,道長逼肯,這世上最難降的妖魔是什么耸黑? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮篮幢,結果婚禮上大刊,老公的妹妹穿的比我還像新娘。我一直安慰自己三椿,他們只是感情好缺菌,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布葫辐。 她就那樣靜靜地躺著,像睡著了一般伴郁。 火紅的嫁衣襯著肌膚如雪耿战。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天焊傅,我揣著相機與錄音剂陡,去河邊找鬼。 笑死狐胎,一個胖子當著我的面吹牛鸭栖,可吹牛的內容都是我干的。 我是一名探鬼主播顽爹,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼纤泵,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了镜粤?” 一聲冷哼從身側響起捏题,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎肉渴,沒想到半個月后公荧,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡同规,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年循狰,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片券勺。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡绪钥,死狀恐怖焰枢,靈堂內的尸體忽然破棺而出瓶颠,到底是詐尸還是另有隱情,我是刑警寧澤舅逸,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布儒拂,位于F島的核電站寸潦,受9級特大地震影響,放射性物質發(fā)生泄漏社痛。R本人自食惡果不足惜见转,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蒜哀。 院中可真熱鬧斩箫,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至寨典,卻和暖如春氛雪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背耸成。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工报亩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人井氢。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓弦追,卻偏偏與公主長得像,于是被迫代替她去往敵國和親花竞。 傳聞我的和親對象是個殘疾皇子劲件,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內容