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,2,3]
my_dict = {'frank':'yangchao', 'ailsa':'liuliu'}
def chain(*args, **kwargs):
    for itemrable in args:
        yield from itemrable
for value in chain(my_list, my_dict, range(5,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, 1500, 3000]), '手機': (394, [88, 100, 98, 108]), '衣服': (1688, [280, 560, 778, 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, [1200, 1500, 3000])

 

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

yield from 功能總結:

  1. 子生成器生產的值,都是直接給調用方;調用發通過.send()發送的值都是直接傳遞給子生成器,如果傳遞None,會調用子生成器的next()方法,如果不是None,會調用子生成器的sen()方法。
  2. 子生成器退出的時候,最后的return EXPR,會觸發一個StopIteration(EXPR)異常
  3. yield from 表達式的值,是子生成器終止時,傳遞給StopIteration異常的第一個參數。
  4. 如果調用的時候出現了StopIteration異常,委托方生成器恢復運行,同時其他的異常向上冒泡。
  5. 傳入委托生成器的異常里,除了GeneratorExit之后,其他所有異常全部傳遞給子生成器的.throw()方法;如果調用.throw()的時候出現StopIteration異常,那么就恢復委托生成器的運行,其他的異常全部向上冒泡
  6. 如果在委托生成器上調用.close()或傳入GeneratorExit異常,會調用子生成器的.close()方法,沒有就不調用,如果在調用.close()時候拋出了異常,那么就向上冒泡,否則的話委托生成器跑出GeneratorExit 異常。 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM