協程:是單線程下的併發,又稱微線程,纖程。英文名Coroutine。一句話說明什麼是線程:協程是一種用戶態的輕量級線程,即協程是由用戶程序自己控制調度的。協程的本質就是在單線程下,由用戶自己控制一個任務遇到io阻塞了就切換另外一個任務去執行,以此來提升效率。(多線程與多進程是搶佔式的,協程是協作式的)
需要強調的是:
1. python的線程屬於內核級別的,即由操作系統控制調度(如單線程遇到io或執行時間過長就會被迫交出cpu執行權限,切換其他線程運行)
2. 單線程內開啓協程,一旦遇到io,就會從應用程序級別(而非操作系統)控制切換,以此來提升效率(!!!非io操作的切換與效率無關)
對比操作系統控制線程的切換,用戶在單線程內控制協程的切換
優點如下:1. 協程的切換開銷更小,屬於程序級別的切換,操作系統完全感知不到,因而更加輕量級
2. 單線程內就可以實現併發的效果,最大限度地利用cpu
缺點如下:1. 協程的本質是單線程下,無法利用多核,可以是一個程序開啓多個進程,每個進程內開啓多個線程,每個線程內開啓協程
2. 協程指的是單個線程,因而一旦協程出現阻塞,將會阻塞整個線程
總結協程特點:1.必須在只有一個單線程裏實現併發
2.修改共享數據不需加鎖
3.用戶程序裏自己保存多個控制流的上下文棧
附加:一個協程遇到IO操作自動切換到其它協程(如何實現檢測IO,yield、greenlet都無法實現,就用到了gevent模塊(select機制))
用yield實現
import time
def customer(name):
print('%s is ready to eat baozi...'%name)
while 1:
baozi=yield
print('customer %s is eating baozi[%s]'%(name,baozi))
time.sleep(1)
def producer():
c1.__next__()
c2.__next__()
n=0
while n<=10:
print('producer is making baozi...')
time.sleep(1)
print('\033[1;32m[producer]\033[0m make baozi[%s] and baozi[%s]'%(n,n+1))
c1.send(n)
c2.send(n+1)
n+=2
if __name__ == '__main__':
c1=customer('alex')
c2=customer('bart')
producer()
用yield實現協程時,遇到io時,仍然會阻塞在原地
如果線程多的話,用yield將非常麻煩於是我們可以使用greenlet模塊
Greenlet
from greenlet import greenlet#導入模塊,如果不存在需要下載
def eat(name):
print('%s eat 1'%name)
gr2.switch('bart') #轉換到gr2
print('%s eat 2'%name)
gr2.switch()
def play(name):
print('%s play 1'%name)
gr1.switch() #轉換到gr1
print('%s play 2'%name)
if __name__ == '__main__':
gr1=greenlet(eat)#傳入要轉換的函數
gr2=greenlet(play)
gr1.switch('alex')#可以在第一次switch時傳入參數,以後都不需要
'''
alex eat 1
bart play 1
alex eat 2
bart play 2
'''
單純的切換(在沒有io的情況下或者沒有重複開闢內存空間的操作),反而會降低程序的執行速度
import time
def a():
n=1
for i in range(100000):
n+=i
def m():
n=1
for i in range(100000):
n*=i
s=time.time()
a()
m()
e=time.time()
print('串行時間爲:',e-s)
from greenlet import greenlet
def a():
n=1
for i in range(100000):
n+=i
g2.switch()
def m():
n=1
for i in range(100000):
n*=i
g1.switch()
g1=greenlet(a)
g2=greenlet(m)
s=time.time()
g1.switch()
e=time.time()
print('協程時間爲:',e-s)
'''
串行時間爲: 0.03602457046508789
協程時間爲: 0.11908483505249023
'''
greenlet只是提供了一種比generator更加便捷的切換方式,當切到一個任務執行時如果遇到io,那就原地阻塞,仍然是沒有解決遇到IO自動切換來提升效率的問題。
單線程裏的這20個任務的代碼通常會既有計算操作又有阻塞操作,我們完全可以在執行任務1時遇到阻塞,就利用阻塞的時間去執行任務2。。。。如此,才能提高效率,這就用到了Gevent模塊。
Gevent
import gevent
def eat(name):
print('%s eat 1'%name)
gevent.sleep(2)
print('%s eat 2'%name)
def play(name):
print('%s play 1'%name)
gevent.sleep(1)
print('%s play 2'%name)
#g1=gevent.spawn(func,1,,2,3,x=4,y=5)創建一個協程對象g1,spawn括號內第一個參數是函數名,如eat,後面可以有多個參數,可以是位置實參或關鍵字實參,都是傳給函數eat的
g1=gevent.spawn(eat,'alex')
g2=gevent.spawn(play,'bart')
gevent.joinall([g1,g2])#相當於g1.join();g2.join()
print('end...')
'''
alex eat 1
bart play 1
bart play 2
alex eat 2
end...
'''
上例gevent.sleep(2)模擬的是gevent可以識別的io阻塞,而time.sleep(2)或其他的阻塞,gevent是不能直接識別的需要用下面一行代碼,打補丁,就可以識別了
from gevent import monkey;monkey.patch_all()必須放到被打補丁者的前面,如time,socket模塊之前
或者我們乾脆記憶成:要用gevent,需要將from gevent import monkey;monkey.patch_all()放到文件的開頭
from gevent import monkey;monkey.patch_all()
import gevent,time
def eat(name):
print('%s eat 1'%name)
time.sleep(2)
print('%s eat 2'%name)
def play(name):
print('%s play 1'%name)
time.sleep(1)
print('%s play 2'%name)
#g1=gevent.spawn(func,1,,2,3,x=4,y=5)創建一個協程對象g1,spawn括號內第一個參數是函數名,如eat,後面可以有多個參數,可以是位置實參或關鍵字實參,都是傳給函數eat的
g1=gevent.spawn(eat,'alex')
g2=gevent.spawn(play,'bart')
gevent.joinall([g1,g2])#相當於g1.join();g2.join()
print('end...')
'''
alex eat 1
bart play 1
bart play 2
alex eat 2
end...
'''
gevent應用(爬蟲)
from gevent import monkey;monkey.patch_all()
import gevent
import requests
import time
def get_page(url):
print('GET: %s' %url)
response=requests.get(url)
if response.status_code == 200:
print('%d bytes received from %s' %(len(response.text),url))
start_time=time.time()
gevent.joinall([
gevent.spawn(get_page,'https://www.python.org/'),
gevent.spawn(get_page,'https://www.baidu.com/'),
gevent.spawn(get_page,'https://github.com/'),
])
stop_time=time.time()
print('run time is %s' %(stop_time-start_time))
'''
GET: https://www.python.org/
GET: https://www.baidu.com/
GET: https://github.com/
2443 bytes received from https://www.baidu.com/
53719 bytes received from https://github.com/
49273 bytes received from https://www.python.org/
run time is 2.2591280937194824
'''