一、協程介紹
協程 ,又被稱為微線程或者纖程,是一種用戶態的輕量級線程,英文名Coroutine,它是實現多任務的一種方式。
其本質就是一個單線程,協程的作用就是在一個線程中人為控制代碼塊的執行順序。
在一個線程中有很多函數,我們稱這些函數為子程序。當一個子程序A在執行過程中可以中斷執行,切換到子程序B,執行子程序B。而在適當的時候子程序B還可以切換回子程序A,去接着子程序A之前中斷的地方(即回到子程序A切換到子程序B之前的狀態)繼續往下執行,這個過程,我們可以稱之為協程。
二、Yield生成器的方式實現協程
在Python中,yield(生成器)可以很容易的實現上述的功能,從一個函數切換到另外一個函數。
由於比較繁瑣,這里不再贅述,可以參考:https://blog.csdn.net/weixin_41599977/article/details/93656042
三、Greenlet模塊
Greenlet是一個用C實現的協程模塊,相比於python自帶的yield,它可以使你在任意函數之間隨意切換,而不需把這個函數先聲明為generator。
安裝:
pip3 install greenlet
from greenlet import greenlet import time def task_1(): while True: print("--This is task 1!--") g2.switch() # 切換到g2中運行 time.sleep(0.5) def task_2(): while True: print("--This is task 2!--") g1.switch() # 切換到g1中運行 time.sleep(0.5) if __name__ == "__main__": g1 = greenlet(task_1) # 定義greenlet對象 g2 = greenlet(task_2) g1.switch() # 切換到g1中運行
運行輸出:
--This is task 1!-- --This is task 2!-- --This is task 1!-- --This is task 2!-- --This is task 1!-- --This is task 2!-- --This is task 1!-- --This is task 2!--
四、Gevent模塊
Greenlet已經實現了協程,但是這個需要人工切換,很麻煩。
Python中還有一個能夠自動切換任務的模塊gevent,其原理是當一個greenlet遇到IO操作(比如網絡、文件操作等)時,就自動切換到其他的greenlet,等到IO操作完成,在適當的時候切換回來繼續執行。
由於IO操作比較耗時,經常使程序處於等待狀態,有了gevent為我們自動切換協程 ,就保證總有greenlet在運行,而不是等待IO。
安裝:
pip3 install gevent
用法:
g1=gevent.spawn(func,1,2,3,x=4,y=5) # 創建一個協程對象g1,spawn括號內第一個參數是函數名,如eat,后面可以有多個參數,可以是位置實參或關鍵字實參,都是傳給函數eat的,spawn是異步提交任務 g2=gevent.spawn(func2) g1.join() #等待g1結束 g2.join() #等待g2結束 有人測試的時候會發現,不寫第二個join也能執行g2,是的,協程幫你切換執行了,但是你會發現,如果g2里面的任務執行的時間長,但是不寫join的話,就不會執行完等到g2剩下的任務了 #或者上述兩步合作一步: gevent.joinall([g1,g2]) g1.value #拿到func1的返回值
遇到IO阻塞時會自動切換任務:
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(eat, 'egon') g2 = gevent.spawn(play, name='egon') g1.join() g2.join() # 或者gevent.joinall([g1,g2])
上例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 # 直接導入即可 import time def eat(): # print() print('eat food 1') time.sleep(2) # 加上monkey就能夠識別到time模塊的sleep了 print('eat food 2') def play(): print('play 1') time.sleep(1) # 來回切換,直到一個I/O的時間結束,這里都是我們個gevent做得,不再是控制不了的操作系統了。 print('play 2') g1 = gevent.spawn(eat) g2 = gevent.spawn(play) gevent.joinall([g1, g2]) print('主')
協程是通過自己的程序(代碼)來進行切換的,只有遇到協程模塊能夠識別的IO操作的時候,程序才會進行任務切換,實現並發效果,如果所有程序都沒有IO操作,那么就基本屬於串行執行了。
五、Python3.x協程
Python3.x系列的協程有很多不同的地方,這里介紹下主要的:
1、asyncio
- asyncio是Python3.4引進的標准庫,直接內置了對IO的支持,asyncio的操作,需要在coroutine中通過yield from完成。
import asyncio @asyncio.coroutine def get_body(i): print(f'start{i}') yield from asyncio.sleep(1) print(f'end{i}') loop = asyncio.get_event_loop() tasks = [get_body(i) for i in range(5)] loop.run_until_complete(asyncio.wait(tasks)) loop.close()
輸出結果:
start4
start0
start1
start3
start2
end4
end0
end1
end3
end2
它的效果是和Gevent一樣的,遇到IO操作的時候,自動切換上下文。
不同的是,它對tasks的操作:task先把這個5個參數不同的函數全部加載進來,然后執行任務,任務執行是無序的。
@asyncio.coroutine把一個generator標記為coroutine類型,然后把這個coroutine扔到eventloop中執行
yield from 語法讓我們方便的調用另一個generator。由於asyncio.sleep()也是一個coroutine,線程不會等待,直接中斷執行下一個消息循環。當asyncio.sleep()返回時,線程可以從yield from拿到返回值(此處是None),然后接着執行下一行語句。
2、async/await
在Python3.5的時候,asyncio
添加了兩個關鍵字
aysnc
和
await
,讓coroutine語法更簡潔。
async
關鍵字可以將一個函數修飾為協程對象,
await
關鍵字可以將需要耗時的操作掛起,一般多是IO操作。
import asyncio async def get_body(i): print(f'start{i}') await asyncio.sleep(1) print(f'end{i}') loop = asyncio.get_event_loop() tasks = [get_body(i) for i in range(5)] loop.run_until_complete(asyncio.wait(tasks)) loop.close()
運行結果:
start3
start4
start1
start0
start2
end3
end4
end1
end0
end2
Python3.7以后的版本使用asyncio.run即可。此函數總是會創建一個新的事件循環並在結束時關閉之。它應當被用作 asyncio 程序的主入口點,理想情況下應當只被調用一次。
import asyncio async def work(x): # 通過async關鍵字定義一個協程 for _ in range(3): print('Work {} is running ..'.format(x)) coroutine_1 = work(1) # 協程是一個對象,不能直接運行 # 方式一: loop = asyncio.get_event_loop() # 創建一個事件循環 result = loop.run_until_complete(coroutine_1) # 將協程對象加入到事件循環中,並執行 print(result) # 協程對象並沒有返回結果,打印None # 方式二: # asyncio.run(coroutine_1) #創建一個新的事件循環,並以coroutine_1為程序的主入口,執行完畢后關閉事件循環
使用asyncio
實現的協程的一些特性:
- 使用
async
修飾返回的協程對象,不能直接執行,需要添加到事件循環event_loop
中執行。 - 協程主要是用於實現並發操作,其本質在同一時刻,只能執行一個任務,並非多個任務同時展開。
- 協程中被掛起的操作,一般都是異步操作,否則使用協程沒有啥意義,不能提高執行效率。
- 協程是在一個單線程中實現的,其並發並未涉及到多線程。
六、為什么要使用協程
我們廣泛使用的Python解釋器是CPython,而CPython解釋器中存在GIL鎖,它的作用就是防止多線程時線程搶占資源,所以在同一時間只允許一個線程在執行,即使在多核CPU情況下也是一樣 ,所以CPU的單核和多核對於多線程的運行效率並沒有多大幫助,還要在線程之間的不停切換。
基於以上情況,在一些多線程的場景時,我們就可以使用協程來代替多線程,並且可以做的更靈活。我們下面來看下協程的優勢:
1、線程是系統調度的,協程是程序員人為調度的,更加靈活,簡化編程模型
2、與多線程相比,協程無需上下文切換的開銷,避免了無意義的調度,提高了性能
3、與多線程相比,協程不需要像線程一樣,無需原子操作鎖定和同步的開銷
所以,在處理一些高並發場景時,有時協程比多線程更加適合,比如做爬蟲時。
參考文章:
https://www.jianshu.com/p/334388949ac9
https://blog.csdn.net/weixin_41599977/article/details/93656042
https://blog.csdn.net/weixin_44251004/article/details/86594117
https://www.cnblogs.com/cheyunhua/p/11017057.html
https://www.cnblogs.com/russellyoung/p/python-zhi-xie-cheng.html
https://www.cnblogs.com/dbf-/p/11143349.html