Hike News
Hike News

Python模塊-協程(yield,greenlet,gevent)

Preface:

  • 不管是線程(threading)或是進程(multiprocessing)都為一種搶占資源(競爭式)的型式執行程序
  • 資源消耗大,且CPU切換耗時

Introduction:

  • 協程為單線程,因此不存在CPU線程之間的切換,效率高
  • 協成為單線程,不存在GIL鎖,且數據取用相對安全
  • 協程為單線程,併發次數幾近無限次,適合用於高併發處理

Notice:

  • 可利用多進程+協程實現CPU多核利用
  • 要是協程中遇到阻塞的情況,因為單線程的原因會造成程序全阻塞

Usage:

使用生成器的yield實現協程操作:

傳統生產者消費者模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def buyer(name):
print(name, "來去準備購物囉!!!")
while True:
buysomething = yield "老闆!我要買這個"
print ("買了一件東西 - %s" % buysomething)


def seller(thing):
buyer_response = next(shopping_person)
print(buyer_response)
shopping_person.send(thing)

print("謝謝光臨!商品重新上架 - %s"%thing)

if __name__ == "__main__":
shopping_list = {"小華":"糖果", "小明":"餅乾", "小馬":"果汁"}
for name,item in shopping_list.items():
shopping_person = buyer(name) # 創建生成器對象
sellsomething = seller( item )

其結果如下

1
2
3
4
5
6
7
8
9
10
11
12
小華 來去準備購物囉!!!
老闆!我要買這個
買了一件東西 - 糖果
謝謝光臨!商品重新上架 - 糖果
小明 來去準備購物囉!!!
老闆!我要買這個
買了一件東西 - 餅乾
謝謝光臨!商品重新上架 - 餅乾
小馬 來去準備購物囉!!!
老闆!我要買這個
買了一件東西 - 果汁
謝謝光臨!商品重新上架 - 果汁

yield可以記住函數執行到的位置
再次調用next()或是send()就可以回到函數內部繼續往下執行

greenlet (python第3方支持協程模塊)

須先安裝

1
pip install gevent

一般執行函數的情況

1
2
3
4
5
6
7
8
9
10
11
def function_1():
print("我是函數_1顯示的第1行")
print("我是函數_1顯示的第2行")

def function_2():
print("我是函數_2顯示的第1行")
print("我是函數_2顯示的第2行")

if __name__ == "__main__":
function_1()
function_2()

結果如下所示

1
2
3
4
我是函數_1顯示的第1行
我是函數_1顯示的第2行
我是函數_2顯示的第1行
我是函數_2顯示的第2行

使用greenlet進行函式間的切換

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from greenlet import greenlet

def function_1():
print("我是函數_1顯示的第1行")
greenlet_test2.switch()
print("我是函數_1顯示的第2行")
greenlet_test2.switch()

def function_2():
print("我是函數_2顯示的第1行")
greenlet_test1.switch()
print("我是函數_2顯示的第2行")

if __name__ == "__main__":
greenlet_test1 = greenlet(function_1)
greenlet_test2 = greenlet(function_2)
greenlet_test1.switch()

其結果如下

1
2
3
4
我是函數_1顯示的第1行
我是函數_2顯示的第1行
我是函數_1顯示的第2行
我是函數_2顯示的第2行

由上述我們可以知道greenlet中的switch方法類似生成器中的next()或是send()方法
可來回的在函數之間進行執行順序的切換

  • 協程中的切換是指執行順序的切換而非CPU的切換

Gevent (python第3方支持協程模塊)

透過gevent實現程序併發效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import gevent

def fun_1():
print("開始執行fun_1")
gevent.sleep()
print("仍在執行fun_1")
gevent.sleep(1)
print("結束fun_1執行")

def fun_2():
print("開始執行fun_2")
gevent.sleep()
print("仍在執行fun_2")
gevent.sleep(2)
print("結束fun_2執行")

if __name__ == "__main__":
g1 = gevent.spawn(fun_1) #創造一個fun_1的gevent物件並啟用此物件
g2 = gevent.spawn(fun_2)
g1.join()
g2.join()

其結果為

1
2
3
4
5
6
開始執行fun_1
開始執行fun_2
仍在執行fun_1
仍在執行fun_2
結束fun_1執行
結束fun_2執行

gevent.sleep()就是用做模擬IO阻塞的情況,亦是交出自己的線程控制權
在其他函式面臨阻塞時進行切換到其他函式繼續執行藉此提高整個程序的執行效率

應用於requests庫

可以看一下如果使用一般的方式進行網頁抓取需要多少時間

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
import time

def GET_url(url):
status = requests.get(url)
data = status.text
print("GET %s status:%s & length:%d"%(url,status,len(data)))

if __name__ == "__main__":
start = time.time()
url_list = ['http://www.taobao.com',
"http://shopee.tw/",
"https://www.autobuy.tw/",
'https://github.com',
'https://tw.yahoo.com',
'http://www.python.org']

for url in url_list:
GET_url(url)

print("spent time: %d sec"%(time.time()-start))

結果如下

1
2
3
4
5
6
7
GET http://www.taobao.com status:<Response [200]> & length:237961
GET http://shopee.tw/ status:<Response [200]> & length:78523
GET https://www.autobuy.tw/ status:<Response [200]> & length:107359
GET https://github.com status:<Response [200]> & length:52086
GET https://tw.yahoo.com status:<Response [200]> & length:443617
GET http://www.python.org status:<Response [200]> & length:48940
spent time: 14 sec

若使用gevent進行切換

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
import gevent
import time

def GET_url(url):
status = requests.get(url)
data = status.text
print("GET %s status:%s & length:%d"%(url,status,len(data)))

if __name__ == "__main__":
start = time.time()
gevent.joinall([
gevent.spawn(GET_url,'http://www.taobao.com'),
gevent.spawn(GET_url,"https://shopee.tw/"),
gevent.spawn(GET_url,"https://www.autobuy.tw/"),
gevent.spawn(GET_url, 'https://github.com'),
gevent.spawn(GET_url, 'https://tw.yahoo.com'),
gevent.spawn(GET_url, 'http://www.python.org'),
])
print("spent time: %d sec"%(time.time()-start))

  • spawn的第一個參數為函數名,第二個參數為函數所需的參數

結果如下

1
2
3
4
5
6
7
GET http://www.taobao.com status:<Response [200]> & length:237961
GET https://shopee.tw/ status:<Response [200]> & length:78523
GET https://www.autobuy.tw/ status:<Response [200]> & length:106504
GET https://github.com status:<Response [200]> & length:52084
GET https://tw.yahoo.com status:<Response [200]> & length:447471
GET http://www.python.org status:<Response [200]> & length:48940
spent time: 4 sec

在windows作業系統上可添加以下代碼使效果更為顯著

1
2
from gevent import monkey
monkey.patch_all()

Reference:

https://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001407503089986d175822da68d4d6685fbe849a0e0ca35000
http://www.cnblogs.com/alex3714/articles/5248247.html