1. 程式人生 > 程式設計 >python 如何引入協程和原理分析

python 如何引入協程和原理分析

相關概念

  • 併發:指一個時間段內,有幾個程式在同一個cpu上執行,但是任意時刻只有一個程式在cpu上執行。比如說在一秒內cpu切換了100個程序,就可以認為cpu的併發是100。
  • 並行:值任意時刻點上,有多個程式同時執行在cpu上,可以理解為多個cpu,每個cpu獨立執行自己程式,互不干擾。並行數量和cpu數量是一致的。

我們平時常說的高併發而不是高並行,是因為cpu的數量是有限的,不可以增加。

形象的理解:cpu對應一個人,程式對應喝茶,人要喝茶需要四個步驟(可以對應程式需要開啟四個執行緒):1燒水,2備茶葉,3洗茶杯,4泡茶。

併發方式:燒水的同時做好2備茶葉,3洗茶杯,等水燒好之後執行4泡茶。這樣比順序執行1234要省時間。

並行方式:叫來四個人(開啟四個程序),分別執行任務1234,整個程式執行時間取決於耗時最多的步驟。

  • 同步(注意同步和非同步只是針對於I/O操作來講的)值呼叫IO操作時,必須等待IO操作完成後才開始新的的呼叫方式。
  • 非同步 指呼叫IO操作時,不必等待IO操作完成就開始新的的呼叫方式。
  • 阻塞指呼叫函式的時候,當前執行緒被掛起。
  • 非阻塞 指呼叫函式的時候,當前執行緒不會被掛起,而是立即返回。

IO多路複用

  sllect, poll, epoll都是IO多路複用的機制。IO多路複用就是通過這樣一種機制:一個程序可以監聽多個描述符,一旦某個描述符就緒(一般是讀就緒和寫就緒),能夠通知程式進行相應的操作。但select,poll,epoll本質上都是同步IO,因為他們都需要在讀寫事件就緒後自己負責進行讀寫(即將資料從核心空間拷貝到應用快取)。也就是說這個讀寫過程是阻塞的。而非同步IO則無需自己負責讀寫,非同步IO的實現會負責把資料從核心拷貝到使用者空間。

select
  select函式監聽的檔案描述符分三類:writefds、readfds、和exceptfds。呼叫後select函式會阻塞,直到描述符就緒(有資料可讀、寫、或者有except)或者超時(timeout指定等待時間,如果立即返回則設定為null),函式返回。當select函式返回後,可以通過遍歷fdset,來找到就緒的描述符。

  優點:良好的跨平臺性(幾乎所有的平臺都支援)
  缺點:單個程序能夠監聽的檔案描述符數量存在最大限制,在linux上一般為1024,可以通過修改巨集定義甚至重新編譯核心來提升,但是這樣也會造成效率降低。

poll

  不同於select使用三個點陣圖來表示fdset的方式,poll使用的是pollfd的指標實現

  pollfd結構包含了要監聽的event和發生的event,不再使用select“引數-值”傳遞的方式。同時pollfd並沒有最大數量限制(但是數量過大之後效能也是會下降)。和select函式一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符。

  從上面看,select和poll都需要在返回後,通過遍歷檔案描述符來獲取已經就緒的socket。事實上,同時連線的大量客戶端在同一時刻可能只有很少的處於就緒狀態,因此隨著監視的描述符數量的增長,其效率也會下降。

epoll

  epoll是在linux2.6核心中國提出的,(windows不支援),是之前的select和poll增強版。相對於select和poll來說,epoll更加靈活,沒有描述符的限制。epoll使用一個檔案描述符管理多個描述符,將使用者關係的檔案描述符的時間存放到核心的一個時間表中。這樣在使用者控制元件和核心控制元件的coppy只需要一次。

如何選擇?

  ①在併發高同時連線活躍度不是很高的請看下,epoll比select好(網站或web系統中,使用者請求一個頁面後隨時可能會關閉)

  ②併發性不高,同時連線很活躍,select比epoll好。(比如說遊戲中資料一但連線了就會一直活躍,不會中斷)

省略章節:由於在用到select的時候需要巢狀多層回撥函式,然後印發一系列的問題,如可讀性差,共享狀態管理困難,出現異常排查複雜,於是引入協程,既操作簡單,速度又快。

協程

對於上面的問題,我們希望去解決這樣幾個問題:

  1. 採用同步的方式去編寫非同步的程式碼,使程式碼的可讀性高,更簡便。
  2. 使用單執行緒去切換任務(就像單執行緒間函式之間的切換那樣,速度超快)

      (1)執行緒是由作業系統切換的,單執行緒的切換意味著我們需要程式設計師自己去排程任務。

      (2)不需要鎖,併發性高,如果單執行緒內切換函式,效能遠高於執行緒切換,併發性更高。

例如我們在做爬蟲的時候:

def get_url(url):
 html = get_html(url) # 此處網路下載IO操作比較耗時,希望切換到另一個函式去執行
 infos = parse_html(html)
# 下載url中的html
def get_html(url):
 pass
# 解析網頁
def parse_html(html):
 pass

意味著我們需要一個可以暫停的函式,對於此函式可以向暫停的地方穿入值。(回憶我們的生成器函式就可以滿足這兩個條件)所以就引入了協程。

生成器進階

  • 生成器不僅可以產出值,還可以接收值,用send()方法。注意:在呼叫send()傳送非None值之前必須先啟動生成器,可以用①next()②send(None)兩種方式啟用
def gen_func():
 html = yield 'http://www.baidu.com' # yield 前面加=號就實現了1:可以產出值2:可以接受呼叫者傳過來的值
 print(html)
 yield 2
 yield 3
 return 'bobby'
if __name__ == '__main__':
 gen = gen_func()
 url = next(gen)
 print(url)
 html = 'bobby'
 gen.send(html) # send方法既可以將值傳遞進生成器內部,又可以重新啟動生成器執行到下一yield位置。

列印結果:
http://www.baidu.com
bobby
  • close()方法。
def gen_func():
 yield 'http://www.baidu.com' # yield 前面加=號就實現了1:可以產出值2:可以接受呼叫者傳過來的值
 yield 2
 yield 3
 return 'bobby'
if __name__ == '__main__':
 gen = gen_func()
 url = next(gen)
 gen.close()
 next(gen)

輸出結果:
StopIteration

特別注意:呼叫close.()之後, 生成器在往下執行的時候就會產生出一個GeneratorExit,單數如果用try捕獲異常的話,就算捕獲了遇到後面還有yield的話,還是不能往下運行了,因為一旦呼叫close方法生成器就終止運行了(如果還有next,就會會產生一個異常)所以我們不要去try捕捉該異常。(此注意可以先忽略)

def gen_func():
 try:
  yield 'http://www.baidu.com' 
 except GeneratorExit:
  pass
 yield 2
 yield 3
 return 'bobby'
if __name__ == '__main__':
 gen = gen_func()
 print(next(gen))
 gen.close()
 next(gen)

輸出結果:
RuntimeError: generator ignored GeneratorExit
  • 呼叫throw()方法。用於丟擲一個異常。該異常可以捕捉忽略。
def gen_func():
 yield 'http://www.baidu.com' # yield 前面加=號就實現了1:可以產出值2:可以接受呼叫者傳過來的值
 yield 2
 yield 3
 return 'bobby'
if __name__ == '__main__':
 gen = gen_func()
 print(next(gen))
 gen.throw(Exception,'Download Error')

輸出結果:
 Download Error

yield from

先看一個函式:from itertools import chain

from itertools import chain
my_list = [1,2,3]
my_dict = {'frank':'yangchao','ailsa':'liuliu'}
for value in chain(my_list,my_dict,range(5,10)): chain()方法可以傳入多個可迭代物件,然後分別遍歷之。
 print(value)

列印結果:
1
2
3
frank
ailsa
5
6
7
8
9

此函式可以用yield from 實現:yield from功能 1:從一個可迭代物件中將值逐個返回。

my_list = [1,'ailsa':'liuliu'}
def chain(*args,**kwargs):
 for itemrable in args:
  yield from itemrable
for value in chain(my_list,10)):
 print(value)

看如下程式碼:

def gen():
 yield 1

def g1(gen):
 yield from gen

def main():
 g = g1(gen)
 g.send(None)

程式碼分析:此程式碼中main呼叫了g1, main就叫作呼叫方, g1叫做委託方, gen 叫做子生成器yield from將會在呼叫方main與子生成器gen之間建立一個雙向通道。(意味著可以直接越過委託方)

例子:當委託方middle()中使用yield from 的時候,呼叫方main直接和子生成器sales_sum形成資料通道。

final_result = {}
def sales_sum(pro_name):
 total = 0
 nums = []
 while True:
  x = yield
  print(pro_name+'銷量',x)
  if not x:
   break
  total += x
  nums.append(x)
 return total,nums #程式執行到return的時候,會將return的返回值返回給委託方,即middle中的final_result[key]
def middle(key):
 while True: #相當於不停監聽sales_sum是否有返回資料,(本例中有三次返回)
  final_result[key] = yield from sales_sum(key)
  print(key +'銷量統計完成!!')
def main():
 data_sets = {
  '面膜':[1200,1500,3000],'手機':[88,100,98,108],'衣服':[280,560,778,70],}

 for key,data_set in data_sets.items():
  print('start key',key)
  m = middle(key)
  m.send(None) # 預激生成器
  for value in data_set:
   m.send(value)
  m.send(None)# 傳送一個None使sales_sum中的x值為None退出while迴圈
 print(final_result)
if __name__ == '__main__':
 main()

結果:
start key 面膜
面膜銷量 1200
面膜銷量 1500
面膜銷量 3000
面膜銷量 None
面膜銷量統計完成!!
start key 手機
手機銷量 88
手機銷量 100
手機銷量 98
手機銷量 108
手機銷量 None
手機銷量統計完成!!
start key 衣服
衣服銷量 280
衣服銷量 560
衣服銷量 778
衣服銷量 70
衣服銷量 None
衣服銷量統計完成!!
{'面膜': (5700,[1200,3000]),'手機': (394,[88,108]),'衣服': (1688,[280,70])}

也許有人會好奇,為什麼不能直接用main()函式直接去呼叫sales_sum呢?加一個委託方使程式碼複雜化了。看以下直接用main()函式直接去呼叫sales_sum程式碼:

def sales_sum(pro_name):
 total = 0
 nums = []
 while True:
  x = yield
  print(pro_name+'銷量',x)
  if not x:
   break
  total += 1
  nums.append(x)
 return total,nums

if __name__ == '__main__':
 my_gen = sales_sum('面膜')
 my_gen.send(None)
 my_gen.send(1200)
 my_gen.send(1500)
 my_gen.send(3000)
 my_gen.send(None)

輸出結果:
面膜銷量 1200
面膜銷量 1500
面膜銷量 3000
面膜銷量 None
Traceback (most recent call last):
 File "D:/MyCode/Cuiqingcai/Flask/test01.py",line 56,in <module>
 my_gen.send(None)
StopIteration: (3,3000])

從上述程式碼可以看出,即使資料return結果出來了,還是會返回一個exception,由此可以看出yield from的一個最大優點就是當子生成器執行時候出現異常,yield from可以直接自動處理這些異常。

yield from 功能總結:

子生成器生產的值,都是直接給呼叫方;呼叫發通過.send()傳送的值都是直接傳遞給子生成器,如果傳遞None,會呼叫子生成器的next()方法,如果不是None,會呼叫子生成器的sen()方法。
子生成器退出的時候,最後的return EXPR,會觸發一個StopIteration(EXPR)異常
yield from 表示式的值,是子生成器終止時,傳遞給StopIteration異常的第一個引數。
如果呼叫的時候出現了StopIteration異常,委託方生成器恢復執行,同時其他的異常向上冒泡。
傳入委託生成器的異常裡,除了GeneratorExit之後,其他所有異常全部傳遞給子生成器的.throw()方法;如果呼叫.throw()的時候出現StopIteration異常,那麼就恢復委託生成器的執行,其他的異常全部向上冒泡
如果在委託生成器上呼叫.close()或傳入GeneratorExit異常,會呼叫子生成器的.close()方法,沒有就不呼叫,如果在呼叫.close()時候丟擲了異常,那麼就向上冒泡,否則的話委託生成器跑出GeneratorExit 異常。

以上就是python 如何引入協程和原理分析的詳細內容,更多關於python 協程的資料請關注我們其它相關文章!