決戰Python之巔(十九) 併發三巨頭之協程

2021-09-25 13:55:59 字數 4397 閱讀 5211

greenlet模組

gevent模組

來來來,複習一下,什麼是程序、執行緒?

程序是資源分配的最小單位,執行緒是cpu排程的最小單位。

按道理來說我們已經算是把cpu的利用率提高很多了。但是我們知道無論是建立多程序還是建立多執行緒來解決問題,都要消耗一定的時間來建立程序、建立執行緒、以及管理他們之間的切換。

隨著我們對於效率的追求不斷提高,基於單執行緒來實現併發又成為乙個新的課題,即只用乙個主線程(很明顯可利用的cpu只有乙個)情況下實現併發。這樣就可以節省建立執行緒所消耗的時間。

為此我們需要先回顧下併發的本質:切換+儲存狀態

併發三巨頭之程序(在程序的狀態部分)。

對於單執行緒下,我們不可避免程式**現io操作,但如果我們能在自己的程式中(即使用者程式級別,而非作業系統級別)控制單執行緒下的多個任務能在乙個任務遇到io阻塞時就切換到另外乙個任務去計算,這樣就保證了該執行緒能夠最大限度地處於就緒態,即隨時都可以被cpu執行的狀態,相當於我們在使用者程式級別將自己的io操作最大限度地隱藏起來,從而可以迷惑作業系統,讓其看到:該執行緒好像是一直在計算,io比較少,從而更多的將cpu的執行許可權分配給我們的執行緒。

協程的本質就是在單執行緒下,由使用者自己控制乙個任務遇到io阻塞了就切換另外乙個任務去執行,以此來提公升效率。為了實現它,我們需要找尋一種可以同時滿足以下條件的解決方案:

協程:是單執行緒下的併發,又稱微執行緒,纖程。英文名coroutine。一句話說明什麼是協程:

協程是一種使用者態的輕量級執行緒,即協程是由使用者程式自己控制排程的。

需要強調的是:

#1. python的執行緒屬於核心級別的,即由作業系統控制排程(如單執行緒遇到io或執行時間過長就會被迫交出cpu執行許可權,切換其他執行緒執行) #2. 單執行緒內開啟協程,一旦遇到io,就會從應用程式級別(而非作業系統)控制切換,以此來提公升效率(!!!非io操作的切換與效率無關)

優點:

缺點:

總結協程特點:

1.必須在只有乙個單執行緒裡實現併發;

2.修改共享資料不需加鎖;

3.使用者程式裡自己儲存多個控制流的上下文棧;

4.附加:乙個協程遇到io操作自動切換到其它協程(如何實現檢測io,yield、greenlet都無法實現,就用到了gevent模組(select機制))

使用這個模組需要自己安裝:

pip install greenlet -i --trusted-host mirrors.aliyun.com

from greenlet import greenlet

def eat(name):

print('%s eat 1' %name)

g2.switch('egon')

print('%s eat 2' %name)

g2.switch()

def play(name):

print('%s play 1' %name)

g1.switch()

print('%s play 2' %name)

g1=greenlet(eat)

g2=greenlet(play)

g1.switch('kris')#可以在第一次switch時傳入引數,以後都不需要

greenlet只是當切到乙個任務執行時如果遇到io,那就原地阻塞,仍然是沒有解決遇到io自動切換來提公升效率的問題。

單執行緒裡的這20個任務的**通常會既有計算操作又有阻塞操作,我們完全可以在執行任務1時遇到阻塞,就利用阻塞的時間去執行任務2…如此,才能提高效率,這就用到了gevent模組。

使用這個模組需要自己安裝:

pip install gevent -i --trusted-host mirrors.aliyun.com

gevent 是乙個第三方庫,可以輕鬆通過gevent實現併發同步或非同步程式設計,在gevent中用到的主要模式是greenlet, 它是以c擴充套件模組形式接入python的輕量級協程。 greenlet全部執行在主程式作業系統程序的內部,但它們被協作式地排程。

#gevent用法介紹

g1=gevent.spawn(func,1,2,3,x=4,y=5)

建立乙個協程物件g1,spawn括號內第乙個引數是函式名,如eat,後面可以有多個引數,可以是位置實參或關鍵字實參,都是傳給函式eat的

g2=gevent.spawn(func2)

g1.join() #等待g1結束

g2.join() #等待g2結束

#或者上述兩步合作一步:gevent.joinall([g1,g2])

g1.value#拿到func1的返回值

import gevent

def eat(name):

print('%s eat 1' %name)

gevent.sleep(2)

print('%s eat 2' %name)

def play(name):

print('%s play 1' %name)

gevent.sleep(1)

print('%s play 2' %name)

g1=gevent.spawn(eat,'egon')

g2=gevent.spawn(play,name='egon')

g1.join()

g2.join()

#或者gevent.joinall([g1,g2])

print('主')

執行結果:

上例gevent.sleep(2)模擬的是gevent可以識別的io阻塞,而time.sleep(2)或其他的阻塞,gevent是不能直接識別的需要用下面一行**,打補丁,就可以識別了

from gevent import monkey;monkey.patch_all()必須放到被打補丁者的前面,如time,socket模組之前

或者我們乾脆記憶成:要用gevent,需要將from gevent import monkey;monkey.patch_all()放到檔案的開頭。

from gevent import monkey;monkey.patch_all()

import gevent

import time

def eat():

print('eat food 1')

time.sleep(2)

print('eat food 2')

def play():

print('play 1')

time.sleep(1)

print('play 2')

g1=gevent.spawn(eat)

g2=gevent.spawn(play)

gevent.joinall([g1,g2])

print('主')

from gevent import spawn,joinall,monkey;monkey.patch_all()

import time

def task(pid):

"""some non-deterministic task

"""time.sleep(0.5)

print('task %s done' % pid)

def synchronous(): # 同步

for i in range(10):

task(i)

def asynchronous(): # 非同步

g_l=[spawn(task,i) for i in range(10)]

joinall(g_l)

print('done')

if __name__ == '__main__':

print('synchronous:')

synchronous()

print('asynchronous:')

asynchronous()

# 上面程式的重要部分是將task函式封裝到greenlet內部執行緒的gevent.spawn。

# 初始化的greenlet列表存放在陣列threads中,此陣列被傳給gevent.joinall 函式,

# 後者阻塞當前流程,並執行所有給定的greenlet任務。執行流程只會在 所有greenlet執行完後才會繼續向下走。

執行結果:

決戰Python之巔(四)

先總體回顧一下第一章的內容 1.三大類程式語言 機器語言 組合語言 高階語言 其中高階語言又分為解釋型 編譯型。2.python的使用者互動 輸入 input 輸出 print。3.注釋 單行使用 多行使用三對單引號 即 4.變數的規範 1.變數名只能是字母 數字或下劃線的任意組合 2.變數名的第乙...

決戰Python之巔(十一)

本篇將介紹遞迴以及函式的內建方法。之前我們已經講過,函式內部可以呼叫其他函式。如果乙個函式在內部呼叫自身,這個函式就是遞迴函式 def func print 遞迴函式 func 這就是乙個遞迴函式,如果你執行這段 的話,理想中會列印無數行 遞迴函式。但事實上並不會 因為目前這個遞迴相當於乙個死迴圈,...

決戰Python之巔(十三)生成器和迭代器

拖了這麼久才來補生成器和迭代器 之前在講列表的時候並沒有經過這個東西,現在我來介紹一下。列表生成式可以用一句話就能生成乙個列表,如a x for x in range 10 這樣就能直接生成乙個0 9的列表。相對於利用for迴圈 while迴圈來說,更簡單快捷。當然你也可以這樣做b x 2 for ...