天天看點

python 協程

程序和線程都會的切換都要消耗時間,儲存線程程序目前狀态以便下次繼續執行。在不怎麼需要cpu的程式中,即相對于IO密集型的程式,協程相對于線程程序資源消耗更小,切換更快,更适用于IO密集型。協程也是單線程的,沒法利用cpu的多核,想利用cpu多核可以通過,程序+協程的方式,又或者程序+線程+協程。

1、協程的簡單實作

協程的原理是通過生成器實作,如下:程式執行到19行,執行consumer函數到13行,next生成器,執行producer函數到8行停下,傳回consumer函數13行繼續往下執行,循環一次再次來到13行,生成器函數将從上次yield停下的地方往下執行。循環如此就完成就完成了并發的效果。

但有人可能會說我用循環順序執行兩個函數結果也一樣啊,如下第2例子。當這裡想說的是,這樣并不能保留函數的執行的位置,隻是簡單的一個函數執行結束換到另一個函數而已,遇到需要cpu等待的操作也沒法切換。在遇到需要cpu等待的操作主動讓出cpu,記住函數執行的位置,下次切換回來繼續執行才能算是并發的運作,提高程式的并發效果。

python 協程
python 協程

1 import time
 2 
 3 
 4 def producer():
 5     while True:
 6         time.sleep(1)
 7         print("+++++ 1個包子", time.strftime("%X"))
 8         yield
 9 
10 
11 def consumer():
12     while True:
13         next(prd)
14         print("----- 1個包子", time.strftime("%X"))
15 
16 
17 if __name__ == "__main__":
18     prd = producer()
19     consumer()
20 
21 
22 # 輸出結果
23 +++++ 1個包子 16:22:30
24 ----- 1個包子 16:22:30
25 +++++ 1個包子 16:22:31
26 ----- 1個包子 16:22:31
27 +++++ 1個包子 16:22:32
28 ----- 1個包子 16:22:32
29 +++++ 1個包子 16:22:33
30 ----- 1個包子 16:22:33      

協程簡單實作生産者與消費者

python 協程
python 協程
1 import time
 2 
 3 
 4 def producer():
 5     time.sleep(1)
 6     print("+++++ 1個包子", time.strftime("%X"))
 7 
 8 
 9 def consumer():
10     print("----- 1個包子", time.strftime("%X"))
11 
12 
13 if __name__ == "__main__":
14     while True:
15         producer()
16         consumer()
17 
18 
19 # 輸出結果
20 +++++ 1個包子 16:22:30
21 ----- 1個包子 16:22:30
22 +++++ 1個包子 16:22:31
23 ----- 1個包子 16:22:31
24 +++++ 1個包子 16:22:32
25 ----- 1個包子 16:22:32
26 +++++ 1個包子 16:22:33
27 ----- 1個包子 16:22:33      

順序執行效果

2、greenlet

greenlet子產品需要安裝,pip install greenlet。greenlet原理是對生成器的封裝。greenlet類提供了一個方法,switch:在需要進行切換的時候切換到指定的協程。

python 協程
python 協程
1 from greenlet import greenlet
 2 import time
 3 
 4 
 5 def producer():
 6     while True:
 7         time.sleep(1)
 8         print("+++++ 1個包子", time.strftime("%X"))
 9         gr2.switch()  # 切換到gr2運作
10 
11 
12 
13 def consumer():
14     while True:
15         print("----- 1個包子", time.strftime("%X"))
16         gr1.switch()  # 切換到gr1運作
17 
18 
19 if __name__ == "__main__":
20     gr1 = greenlet(producer)
21     gr2 = greenlet(consumer)
22     gr1.switch()  # 切換到gr1運作
23 
24 
25 # 輸出結果
26 +++++ 1個包子 09:39:45
27 ----- 1個包子 09:39:45
28 +++++ 1個包子 09:39:46
29 ----- 1個包子 09:39:46
30 +++++ 1個包子 09:39:47
31 ----- 1個包子 09:39:47      

greenlet簡單實作

3、gevent

gevent子產品也需要安裝,pip install gevent。gevent是對gevent的再次封裝,能自動識别耗時操作切換到其它協程。注意gevent遇到耗時操作才會切換協程運作,沒有遇到耗時操作是不會主動切換的。

gevent.spawn(*args, **kwargs)    不定長參數中的第一個參數為協程執行的方法fn,其餘的依次為 fn 的參數。開啟了協程後要調用join方法。

gevent子產品中識别耗時的操作有兩種方式,① 使用gevent子產品中重寫的類。如,gevent.socket  gevent.sleep  ② 打更新檔的方式,在所有的代碼前。from gevent import  monkey 導入這個子產品,monkey.patch_all()調用這個方法。

推薦使用第二種方式,這樣就不用更改已經寫好的代碼

python 協程
python 協程
1 import time
 2 import gevent
 3 
 4 
 5 def producer():
 6     for i in range(3):
 7         time.sleep(1)
 8         print("+++++ 1個包子", name, time.strftime("%X"))
 9 
10 
11 def consumer():
12     for i in range(3):
13         time.sleep(1)
14         print("----- 1個包子", name, time.strftime("%X"))
15 
16 
17 if __name__ == "__main__":
18     g1 = gevent.spawn(producer, "zhangsan")
19     g2 = gevent.spawn(consumer, "lisi")
20     g1.join()
21     g2.join()
22 
23 
24 # 輸出結果
25 +++++ 1個包子 zhangsan 10:42:38
26 +++++ 1個包子 zhangsan 10:42:39
27 +++++ 1個包子 zhangsan 10:42:40
28 ----- 1個包子 lisi 10:42:41
29 ----- 1個包子 lisi 10:42:42
30 ----- 1個包子 lisi 10:42:43      

正常情況下gevent并不會識别耗時操作

python 協程
python 協程
1 import time
 2 import gevent
 3 
 4 
 5 def producer():
 6     for i in range(3):
 7         gevent.sleep(1)
 8         print("+++++ 1個包子", time.strftime("%X"))
 9 
10 
11 def consumer():
12     for i in range(3):
13         gevent.sleep(1)
14         print("----- 1個包子", time.strftime("%X"))
15 
16 
17 if __name__ == "__main__":
18     g1 = gevent.spawn(producer)
19     g2 = gevent.spawn(consumer)
20     g1.join()
21     g2.join()
22 
23 
24 # 輸出結果
25 +++++ 1個包子 10:43:04
26 ----- 1個包子 10:43:04
27 +++++ 1個包子 10:43:05
28 ----- 1個包子 10:43:05
29 +++++ 1個包子 10:43:06
30 ----- 1個包子 10:43:06      

gevent識别耗時操作方式1,使用gevent中的子產品

python 協程
python 協程
1 import time
 2 import gevent
 3 from gevent import monkey
 4 monkey.patch_all()
 5 
 6 
 7 def producer():
 8     for i in range(3):
 9         time.sleep(1)
10         print("+++++ 1個包子", time.strftime("%X"))
11 
12 
13 def consumer():
14     for i in range(3):
15         time.sleep(1)
16         print("----- 1個包子", time.strftime("%X"))
17 
18 
19 if __name__ == "__main__":
20     g1 = gevent.spawn(producer)
21     g2 = gevent.spawn(consumer)
22     g1.join()
23     g2.join()
24 
25 
26 # 輸出結果
27 +++++ 1個包子 10:44:04
28 ----- 1個包子 10:44:04
29 +++++ 1個包子 10:44:05
30 ----- 1個包子 10:44:05
31 +++++ 1個包子 10:44:06
32 ----- 1個包子 10:44:06      

gevent識别耗時操作方式2,打更新檔

當開啟的協程很多的時候,一個個的調用join方法就有點麻煩,是以gevent提供了一個方法joinall(),可以一次join所有的協程。joinall() 方法傳參一個清單,清單包含了所有的協程。

python 協程
python 協程
1 import time
 2 import gevent
 3 from gevent import monkey
 4 monkey.patch_all()
 5 
 6 
 7 def producer(name):
 8     for i in range(3):
 9         time.sleep(1)
10         print("+++++ 1個包子", name, time.strftime("%X"))
11 
12 
13 def consumer(name):
14     for i in range(3):
15         time.sleep(1)
16         print("----- 1個包子",  name, time.strftime("%X"))
17 
18 
19 if __name__ == "__main__":
20     gevent.joinall([gevent.spawn(producer, "zhangsan"), gevent.spawn(consumer, "lisi")])
21 
22 
23 # 輸出結果
24 +++++ 1個包子 zhangsan 10:51:34
25 ----- 1個包子 lisi 10:51:34
26 +++++ 1個包子 zhangsan 10:51:35
27 ----- 1個包子 lisi 10:51:35
28 +++++ 1個包子 zhangsan 10:51:36
29 ----- 1個包子 lisi 10:51:36      

joinall

4、協程應用,并發伺服器

服務端收到用戶端消息,并原樣發送回去

python 協程
python 協程
1 import socket
 2 import gevent
 3 from gevent import monkey
 4 
 5 monkey.patch_all()
 6 
 7 
 8 def fn(conn):
 9     msg = conn.recv(1024).decode("utf-8")
10     print("服務的收到>>>", msg)
11     conn.send(msg.encode("utf-8"))
12 
13 
14 sk = socket.socket()
15 sk.bind(("127.0.0.1", 8899))
16 sk.listen()
17 while True:
18     conn, addr = sk.accept()
19     print("已連接配接伺服器-->", addr)
20     gevent.spawn(fn, conn)
21 sk.close()
22 
23 
24 # 輸出結果
25 已連接配接伺服器--> ('127.0.0.1', 53878)
26 已連接配接伺服器--> ('127.0.0.1', 53879)
27 已連接配接伺服器--> ('127.0.0.1', 53880)
28 服務的收到>>> client1
29 服務的收到>>> client2
30 服務的收到>>> client3      

server

python 協程
python 協程
1 import socket
 2 
 3 sk = socket.socket()
 4 sk.connect(("127.0.0.1", 8899))
 5 msg = input("用戶端發送的内容>>> ")
 6 sk.send(msg.encode("utf-8"))
 7 msg = sk.recv(1024).decode("utf-8")
 8 print("用戶端收到>>>", msg)
 9 sk.close()
10 
11 
12 # 輸出結果
13 用戶端發送的内容>>> client1
14 用戶端收到>>> client1      

client

繼續閱讀