Python協程(一) 概述


一、協程介紹

協程 ,又被稱為微線程或者纖程,是一種用戶態的輕量級線程,英文名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添加了兩個關鍵字 aysncawait,讓coroutine語法更簡潔。
async關鍵字可以將一個函數修飾為協程對象, await關鍵字可以將需要耗時的操作掛起,一般多是IO操作。
它們是針對coroutine的新語法,只需要把 @asyncio.coroutine替換為 asyncyield from替換為 await
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

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM