1. 程式人生 > >多工實現-協程

多工實現-協程

協程,又稱微執行緒,纖程。英文名Coroutine。

協程是啥

首先我們得知道協程是啥?協程其實可以認為是比執行緒更小的執行單元。 為啥說他是一個執⾏單元,因為他自帶CPU上下文。這樣只要在合適的時機,我們可以把一個協程 切換到另一個協程。 只要這個過程中儲存或恢復 CPU上下文那麼程式還是可以執行的。 

通俗的理解:在一個執行緒中的某個函式,可以在任何地方儲存當前函式的一些臨時變數等資訊,然後切換到另外一個函式中執行,注意不是通過呼叫函式的方式做到的,並且切換的次數以及什麼時候再切換到原來的函式都由開發者自己確定

協程和執行緒差異

那麼這個過程看起來比執行緒差不多。其實不然, 執行緒切換從系統層面遠不止儲存和恢復 CPU上下文這麼簡單。 作業系統為了程式執行的高效性每個執行緒都有自己快取Cache等等資料,作業系統還會幫你做這些資料的恢復操作。所以執行緒的切換非常耗效能。但是協程的切換隻是單純的操作CPU的上下文,所以一秒鐘切換個上百萬次系統都抗的住。

協程的問題

但是協程有一個問題,就是系統並不感知,所以作業系統不會幫你做切換。那麼誰來幫你做切換?讓需要執行的協程更多的獲得CPU時間才是問題的關鍵。

例子

目前的協程框架一般都是設計成 1:N 模式。所謂 1:N 就是一個執行緒作為一個容器裡面放置多個協程。 那麼誰來適時的切換這些協程?答案是有協程自己主動讓出CPU,也就是每個協程池裡面有一個排程器, 這個排程器是被動排程的。意思就是他不會主動排程。而且當一個協程發現自己執行不下去了(比如非同步等待網路的資料回來,但是當前還沒有資料到), 這個時候就可以由這個協程通知排程器,這個時候執行到排程器的程式碼,排程器根據事先設計好的排程演算法找到當前最需要CPU的協程。 切換這個協程的CPU上下文把CPU的執行權交給這個協程,直到這個協程出現執行不下去需要等等的情況,或者它呼叫主動讓出CPU的API之類,觸發下一次排程。

那麼這個實現有沒有問題?

其實是有問題的,假設這個執行緒中有一個協程是CPU密集型的他沒有IO操作, 也就是自己不會主動觸發排程器排程的過程,那麼就會出現其他協程得不到執行的情況, 所以這種情況下需要程式設計師自己避免。這是一個問題,假設業務開發的人員並不懂這個原理的話就可能會出現問題。

協程的好處

在IO密集型的程式中由於IO操作遠遠慢於CPU的操作,所以往往需要CPU去等IO操作。 同步IO下系統需要切換執行緒,讓作業系統可以在IO過程中執行其他的東西。 這樣雖然程式碼是符合人類的思維習慣但是由於大量的執行緒切換帶來了大量的效能的浪費,尤其是IO密集型的程式。

所以人們發明了非同步IO。就是當資料到達的時候觸發我的回撥。來減少執行緒切換帶來效能損失。 但是這樣的壞處也是很大的,主要的壞處就是操作被“分片” 了,程式碼寫的不是 “一氣呵成” 這種。 而是每次來段資料就要判斷 資料夠不夠處理哇,夠處理就處理吧,不夠處理就在等等吧。這樣程式碼的可讀性很低,其實也不符合人類的習慣。

但是協程可以很好解決這個問題。比如 把一個IO操作 寫成一個協程。當觸發IO操作的時候就自動讓出CPU給其他協程。要知道協程的切換很輕的。 協程通過這種對非同步IO的封裝 既保留了效能也保證了程式碼的容易編寫和可讀性。在高IO密集型的程式下很好。但是高CPU密集型的程式下沒啥好處。

協程一個簡單實現

import time

def A():
    while True:
        print("----A---")
        yield
        time.sleep(0.5)

def B(c):
    while True:
        print("----B---")
        c.next()
        time.sleep(0.5)

if __name__=='__main__':
    a = A()
    B(a)

執行結果:

--B--
--A--
--B--
--A--
--B--
--A--
--B--
--A--
--B--
--A--
--B--
--A--
...省略...