1. 程式人生 > 程式設計 >協程Python 中實現多工耗資源最小的方式

協程Python 中實現多工耗資源最小的方式

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

協程是 Python 中另外一種實現多工的方式,只不過比執行緒更小,佔用更小執行單元(理解為需要的資源)。

為啥說它是一個執行單元,因為它自帶 CPU 上下文。這樣只要在合適的時機, 我們可以把一個協程 切換到另一個協程。 只要這個過程中儲存或恢復 CPU上下文那麼程式還是可以執行的。

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

協程和執行緒差異

在實現多工時,執行緒切換從系統層面遠不止儲存和恢復 CPU上下文這麼簡單。

作業系統為了程式執行的高效性每個執行緒都有自己快取 Cache 等等資料,作業系統還會幫你做這些資料的恢復操作,所以執行緒的切換非常耗效能。

但是協程的切換隻是單純的操作 CPU 的上下文,所以一秒鐘切換個上百萬次系統都抗得住。

之前我們講過 yield 關鍵字,現在就用它來實現多工。

例子:

import time

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

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

def main():
  t1 = task_1()
  t2 = task_2()
  while True:
    next(t1)
    next(t2)

if __name__ == "__main__":
  main()

執行過程:

先讓 t1 執行一會,當 t1 遇到 yield 的時候,再返回到 main() 迴圈的地方,然後執行 t2 , 當它遇到 yield 的時候,再次切換到 t1 中,這樣 t1 和 t2 就交替執行,最終實現了多工,協程。

執行結果:

協程Python 中實現多工耗資源最小的方式

greenlet

為了更好使用協程來完成多工,Python 中的 greenlet 模組對其封裝,從而使得切換任務變的更加簡單。

首先你要安裝一下 greenlet 模組。

pip3 install greenlet
from greenlet import greenlet
import time

def test1():
  while True:
    print("---A--")
    gr2.switch()
    time.sleep(0.5)

def test2():
  while True:
    print("---B--")
    gr1.switch()
    time.sleep(0.5)

gr1 = greenlet(test1)
gr2 = greenlet(test2)

# 切換到gr1中執行
gr1.switch()

執行結果:

協程Python 中實現多工耗資源最小的方式

和我們之前用 yield 實現的效果基本一樣,greenlet 其實是對 yield 進行了簡單的封裝。

greenlet 實現多工要比 yield 更簡單,但是我們以後還是不用它。

上面例子中的延時是0.5秒,如果延遲是100秒,那麼程式就會卡住100秒,就算有其他需要執行的任務,系統也不會切換過去,這100秒的時間是無法利用的。

這個問題下面來解決。

gevent

greenlet 已經實現了協程,但是還是得進行人工切換,是不是覺得太麻煩了。

Python 還有一個比 greenlet 更強大的並且能夠自動切換任務的模組 gevent。

gevent 是對 greenlet 的再次封裝。

其原理是當一個 greenlet 遇到 IO(指的是input output 輸入輸出,比如網路、檔案操作等)操作時,比如訪問網路,就自動切換到其他的 greenlet,等到 IO 操作完成,再在適當的時候切換回來繼續執行。

由於 IO 操作非常耗時,經常使程式處於等待狀態,有了gevent 為我們自動切換協程,就保證總有 greenlet 在執行,而不是等待 IO。

首先還是得先安裝 gevent。

pip3 install gevent

例子:

import gevent

def f(n):
  for i in range(n):
    print(gevent.getcurrent(),i)

g1 = gevent.spawn(f,3)
g2 = gevent.spawn(f,3)
g3 = gevent.spawn(f,3)
g1.join()
g2.join()
g3.join()

執行結果:

<Greenlet at 0x35aae40: f(3)> 0
<Greenlet at 0x35aae40: f(3)> 1
<Greenlet at 0x35aae40: f(3)> 2
<Greenlet at 0x374a780: f(3)> 0
<Greenlet at 0x374a780: f(3)> 1
<Greenlet at 0x374a780: f(3)> 2
<Greenlet at 0x374a810: f(3)> 0
<Greenlet at 0x374a810: f(3)> 1
<Greenlet at 0x374a810: f(3)> 2

可以看到,3個 greenlet 是依次執行而不是交替執行。

這還無法判斷 gevent 是否實現了多工的效果,最好的判斷情況是在執行結果中 0 1 2 不按順序出現。

在 gevent 的概念中,我們提到 gevent 在遇到延時的時候會自動切換任務。

那麼,我們先給上面的例子新增延時,再看效果。

import gevent
import time

def f(n):
  for i in range(n):
    print(gevent.getcurrent(),i)
    time.sleep(0.5)

g1 = gevent.spawn(f,3)
g1.join()
g2.join()
g3.join()

執行結果:

<Greenlet at 0x36aae40: f(3)> 0
<Greenlet at 0x36aae40: f(3)> 1
<Greenlet at 0x36aae40: f(3)> 2
<Greenlet at 0x384a780: f(3)> 0
<Greenlet at 0x384a780: f(3)> 1
<Greenlet at 0x384a780: f(3)> 2
<Greenlet at 0x384a810: f(3)> 0
<Greenlet at 0x384a810: f(3)> 1
<Greenlet at 0x384a810: f(3)> 2

在添加了延時之後,執行結果並沒有改變。

其實,gevent 要的不是 time.sleep() 的延時,而是 gevent.sleep() 的延時。

import gevent

def f(n):
  for i in range(n):
    print(gevent.getcurrent(),i)
    gevent.sleep(0.5)

g1 = gevent.spawn(f,3)
g1.join()
g2.join()
g3.join()

join 還有一種更簡單的寫法。

import time
import gevent

def f(n):
  for i in range(n):
    print(gevent.getcurrent(),i)
    gevent.sleep(0.5)

gevent.joinall([
  gevent.spawn(f,3),gevent.spawn(f,3)
])

一般都是後面的這種寫法。

執行結果:

<Greenlet at 0x2e5ae40: f(3)> 0
<Greenlet at 0x2ffa780: f(3)> 0
<Greenlet at 0x2ffa810: f(3)> 0
<Greenlet at 0x2e5ae40: f(3)> 1
<Greenlet at 0x2ffa780: f(3)> 1
<Greenlet at 0x2ffa810: f(3)> 1
<Greenlet at 0x2e5ae40: f(3)> 2
<Greenlet at 0x2ffa780: f(3)> 2
<Greenlet at 0x2ffa810: f(3)> 2

這下終於實現多工的效果了,gevent 在遇到延時的時候,就自動切換到其他任務。

這裡是將 time 中的 sleep 換成了 gevent 中的 sleep。

那如果有網路程式,網路程式中也有許多堵塞,比如 connect, recv,accept,需要不需要換成 gevent 中的對應方法。

理論上來說,是要換的。如果想用 gevent,那麼就要把所有的延時操作,堵塞這一類的函式,統統換成 gevent 中的對應方法。

那有個問題,萬一我的程式碼已經寫了10萬行了,這換起來怎麼破......

有什麼辦法不需要手動修改麼,有,打個補丁即可。

import time
import gevent
from gevent import monkey

# 有耗時操作時需要
# 將程式中用到的耗時操作的程式碼,換為gevent中自己實現的模組
monkey.patch_all() 

def f(n):
  for i in range(n):
    print(gevent.getcurrent(),3)
g1.join()
g2.join()
g3.join()

monkey.patch_all() 會自動去檢查程式碼,將所有會產生延時堵塞的方法,都自動換成 gevent 中的方法。

執行結果:

<Greenlet at 0x3dd91e0: f(3)> 0
<Greenlet at 0x3dd9810: f(3)> 0
<Greenlet at 0x3dd99c0: f(3)> 0
<Greenlet at 0x3dd91e0: f(3)> 1
<Greenlet at 0x3dd9810: f(3)> 1
<Greenlet at 0x3dd99c0: f(3)> 1
<Greenlet at 0x3dd91e0: f(3)> 2
<Greenlet at 0x3dd9810: f(3)> 2
<Greenlet at 0x3dd99c0: f(3)> 2

總結:

通過利用延時的時間去做其他任務,把時間都利用起來,這就是協程最大的意義。

到此這篇關於協程Python 中實現多工耗資源最小的方式的文章就介紹到這了,更多相關Python多工耗資源最小方式內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!