Python-asyncio的使用-2

asyncio模塊提供了使用協程構建併發應用的工具。它使用一種單線程單進程的方式實現併發,應用的各個部分彼此合作,可以顯示的切換任務,一般會在程序阻塞I/O操作的時候發生上下文切換如等待讀寫文件,或者請求網絡。同時asyncio也支持調度代碼在將來的某個特定事件運行,從而支持一個協程等待另一個協程完成,以處理系統信號和識別其他一些事件。

對於其他的併發模型大多數採用的都是線性的方式編寫。並且依賴於語言運行時系統或操作系統底層的底層線程或進程來適當的改變上下文,而基於asyncio的應用要求應用代碼顯示的處理上下文切換。asyncio提供的框架以事件循環爲中心,程序開啓一個無限的循環,程序會把一些函數註冊到事件循環當中,當滿足事件發生的時候,調用相應的協程函數。

鎖(Lock)

鎖可以用來保護一個共享資源的訪問,只有鎖的持有者可以使用這個資源。如果有多個請求需要用到這個鎖,那麼會將其阻塞,以保證一次只有一個持有者。它是在解鎖狀態下創建的,它有兩個基本方法:acquire()release()。當狀態解除鎖定時,acquire()將狀態更改爲locked並立即返回。當狀態被鎖定時,acquire()阻塞,直到另外一個協同程序中的release()調用將其改爲解鎖,然後acquire()調用將其重置爲鎖定並返回。

release()方法只能在鎖定狀態下調用,它將狀態更改爲解鎖並立即返回。如果試圖釋放一個未鎖定的鎖,將引發一個RuntimeError。此類不是線程安全的!請看下面例子:

import asyncio
from functools import partial

async def release(lock):
    print("relase-1: {}".format(lock.locked()))
    lock.release()
    print("relase-2: {}".format(lock.locked()))
    
async def coroutine_1(lock):
    print("進入coroutine_1: {}".format(lock.locked()))
    await lock.acquire()
    print("coroutine_1此時鎖的狀態: {}".format(lock.locked()))
    lock.release()
    print("coroutine_1此時鎖的狀態-2: {}".format(lock.locked()))
    
async def coroutine_2(lock):
    print("進入coroutine_2: {}".format(lock.locked()))
    async with lock:
        print("coroutine_2此時鎖的狀態: {}".format(lock.locked()))
        
    print("coroutine_2此時鎖的狀態-2: {}".format(lock.locked()))
    
async def coroutine_3(lock):
    print("進入coroutine_3: {}".format(lock.locked()))
    try:
        lock.release()
    except RuntimeError as e:
        print("觸發RuntimeError錯誤")
        
async def main(loop):
    print("進入主協程")
    # 創建一個鎖
    lock = asyncio.Lock()
    loop.call_later(0.1, partial(release, lock))
    await asyncio.wait([coroutine_1(lock), coroutine_2(lock), coroutine_3(lock)])
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        loop.close()

可能輸出的結果如下:

進入主協程
進入coroutine_1: False
coroutine_1此時鎖的狀態: True
coroutine_1此時鎖的狀態-2: False
進入coroutine_2: False
coroutine_2此時鎖的狀態: True
coroutine_2此時鎖的狀態-2: False
進入coroutine_3: False
觸發RuntimeError錯誤

通過上面代碼可以得出以下結論:

  • lock.acquire()需要加await
  • lock.release()不需要加await
  • 在加鎖之前coroutine_1coroutine_2coroutine_3是併發執行的
  • 鎖有兩種使用方式,一是像cortouine_1中使用await lock.acquire()加鎖,這種方式需要在結束的使用調用lock.release()來釋放鎖;二是像coroutine_2中使用async with lock異步上下文進行鎖定,這種方式不需要手動進行釋放操作
  • 如果沒有使用acquire進行加鎖,卻試圖使用release去釋放鎖,那麼將會引發RuntimeError異常

常用方法如下:

  • locked(): 如果獲得了鎖,則返回True,否則返回False
  • acquire(): 獲取鎖。此方法將一直鎖定直到解鎖,將其設置爲locked,並返回True
  • release(): 釋放鎖。當鎖被鎖定,重置爲解鎖。無返回值

事件(Event)

asyncio.Event基於threading.Event,允許多個消費者等待某個事件發生,而不必尋找一個特定值與通知關聯!事件管理一個標識,該標識可以通過set()方法設置爲True,通過clear()方法重置爲False,wait()方法標阻塞,直到標記爲True。該標識最初爲False,此類不是線程安全的!

import asyncio
from functools import partial

def callback(event):
    print("callback當前Event狀態:{}".format(event.is_set()))
    event.set()
    print("callback當前Event狀態-2:{}".format(event.is_set()))
    
async def child(name, event):
    print("{}當前Event狀態: {}".format(name, event.is_set()))
    await event.wait()
    print("{}當前Event狀態: {}".format(name, event.is_set()))
    
async def main(loop):
    event = asyncio.Event()
    print("Event創建之後的狀態爲: {}".format(event.is_set()))
    loop.call_later(0.1, partial(callback, event))
    tasks = [child("name{}".format(i), event) for i in range(5)]
    await asyncio.wait(tasks)
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        loop.close()

可能輸出的結果如下:

Event創建之後的狀態爲: False
name2當前Event狀態: False
name3當前Event狀態: False
name4當前Event狀態: False
name0當前Event狀態: False
name1當前Event狀態: False
callback當前Event狀態:False
callback當前Event狀態-2:True
name2當前Event狀態: True
name3當前Event狀態: True
name4當前Event狀態: True
name0當前Event狀態: True
name1當前Event狀態: True

從結果可以看出,一個觸發了事件,child就會立即啓動,不需要得到事件對象上唯一的鎖!

常用方法如下:

  • clear(): 將內部標識重置爲False
  • is_set(): 返回內部標識狀態
  • set(): 將內部標識設置爲True,所有等待它爲True的協程將被喚醒
  • wait(): 阻塞,直到內部標識設置爲True

條件(condition)

Condition的做法與Event類似,只不過不是通知所有的協程等待的協程,被喚醒的等待協程數目由notify()的一個參數控制,此類不是線程安全的。請看如下代碼:

import asyncio

async def child(cond, name):
    print("{}進入child, cond狀態爲: {}".format(name, cond.locked()))
    async with cond:
        await cond.wait()
        print("{}在child開始執行, cond狀態爲: {}".format(name, cond.locked()))
        
    print("{}在child執行完畢, cond狀態爲: {}".format(name, cond.locked()))

async def coroutine_1(cond):
    print("進入coroutine_1, cond狀態爲: {}".format(cond.locked()))
    await asyncio.sleep(2)
    for i in range(1, 4):
        async with cond:
            print("coroutine_1-資源變得可用")
            cond.notify(n=i)
            
        await asyncio.sleep(1)
        
async def coroutine_2(cond):
    print("進入coroutine_2, cond狀態爲: {}".format(cond.locked()))
    await asyncio.sleep(2)
    async with cond:
        print("coroutine_2-資源變得可用")
        cond.notify_all()
        
async def main(loop):
    print("進入main協程")
    cond = asyncio.Condition()
    print("condition的狀態爲:{}".format(cond.locked()))
    loop.create_task(coroutine_1(cond))
    tasks = [child(cond, "name{}".format(i)) for i in range(1, 6)]
    await asyncio.wait(tasks)
    loop.create_task(coroutine_2(cond))
    tasks = [child(cond, "name{}".format(i)) for i in range(6, 11)]
    await asyncio.wait(tasks)

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        loop.close()

輸出結果如下:

進入main協程
condition的狀態爲:False
進入coroutine_1, cond狀態爲: False
name3進入child, cond狀態爲: False
name4進入child, cond狀態爲: False
name1進入child, cond狀態爲: False
name5進入child, cond狀態爲: False
name2進入child, cond狀態爲: False
coroutine_1-資源變得可用
name3在child開始執行, cond狀態爲: True
name3在child執行完畢, cond狀態爲: False
coroutine_1-資源變得可用
name4在child開始執行, cond狀態爲: True
name4在child執行完畢, cond狀態爲: False
name1在child開始執行, cond狀態爲: True
name1在child執行完畢, cond狀態爲: False
coroutine_1-資源變得可用
name5在child開始執行, cond狀態爲: True
name5在child執行完畢, cond狀態爲: False
name2在child開始執行, cond狀態爲: True
name2在child執行完畢, cond狀態爲: False
進入coroutine_2, cond狀態爲: False
name10進入child, cond狀態爲: False
name9進入child, cond狀態爲: False
name8進入child, cond狀態爲: False
name6進入child, cond狀態爲: False
name7進入child, cond狀態爲: False
coroutine_2-資源變得可用
name10在child開始執行, cond狀態爲: True
name10在child執行完畢, cond狀態爲: False
name9在child開始執行, cond狀態爲: True
name9在child執行完畢, cond狀態爲: False
name8在child開始執行, cond狀態爲: True
name8在child執行完畢, cond狀態爲: False
name6在child開始執行, cond狀態爲: True
name6在child執行完畢, cond狀態爲: False
name7在child開始執行, cond狀態爲: True
name7在child執行完畢, cond狀態爲: False

通過輸出結果進行分析,使用notify方法每次通知n個child,notify_all方法一次性的通知全部child。

常用方法如下:

  • acquire(): 獲取鎖。此方法將一直鎖定直到解鎖,將其設置爲locked,並返回True
  • notify(n=1): 默認情況下,喚醒一個在此條件下等待的協程(如果有的話)。如果調用的協程在調用此方法時沒有獲得鎖,則會引發RuntimeError異常。此方法最多喚醒n個等待條件變量的協程
  • locked(): 如果獲得了鎖,則返回True,否則返回False
  • notify_all(): 喚醒所有此條件下等待的協程。如果調用的協程在調用此方法時沒有獲得鎖,則會引發RuntimeError異常。
  • release(): 釋放鎖。當鎖被鎖定,重置爲解鎖。無返回值
  • wait(): 阻塞,直到內部標識設置爲True
  • wait_for(predicate): 阻塞,直到predicate的返回值爲True

wait_for使用如下:

async def child(cond, name):
    print("{}進入child, cond狀態爲: {}".format(name, cond.locked()))
    async with cond:
        await cond.wait_for(coroutine)
        print("{}在child開始執行, cond狀態爲: {}".format(name, cond.locked()))

    print("{}在child執行完畢, cond狀態爲: {}".format(name, cond.locked()))

def coroutine():
    time.sleep(2)
    return True

Condition對鎖的一些操作與Lock一樣!

隊列

Queue

Queue(maxsize=0, *, loop=None)先進先出隊列,如果maxsize小於或等於0,則隊列大小爲無窮大。如果它是一個大於0的整數,那麼當隊列達到maxsize時,put()的yield將阻塞,直到get()刪除一個項目爲止。此類是線程不安全的!

與標準庫Queue不同,qsize()可以可靠的直到此隊列的大小,因爲在調用qsize()和隊列執行操作之間不會中斷單線程異步應用程序。請看如下列子:

import asyncio

async def get_item(queue):
    item = await queue.get()
    print("get_item: {}".format(item))
    return item
    
async def main():
    queue = asyncio.Queue()
    [await queue.put("name{}".format(i)) for i in range(5)]
    tasks = [get_item(queue) for _ in range(5)]
    await asyncio.wait(tasks)
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

輸出結果如下:

get_item: name0
get_item: name1
get_item: name2
get_item: name3
get_item: name4

LifoQueue

LifoQueue(maxsize=0, *, loop=None)Queue的子類,先進後出隊列。將上面代碼修改爲使用LifoQueue,如下:

import asyncio

async def get_item(queue):
    item = await queue.get()
    print("get_item: {}".format(item))
    return item
    
async def main():
    queue = asyncio.LifoQueue()
    [await queue.put("name{}".format(i)) for i in range(5)]
    tasks = [get_item(queue) for _ in range(5)]
    await asyncio.wait(tasks)
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

輸出結果如下:

get_item: name4
get_item: name3
get_item: name2
get_item: name1
get_item: name0

PriorityQueue

PriorityQueue(maxsize=0, *, loop=None)Queue的子類,優先級隊列,優先級的值越小,越先執行。put()操作與上面的兩種隊列有所不同,"項目"的形式是一個元組:(priority_number, data),priority_number爲優先級數值!將上面代碼修改爲使用PriorityQueue,如下:

import asyncio
import random

async def get_item(queue):
    item = await queue.get()
    print("get_item: {}".format(item))
    return item
    
async def main():
    queue = asyncio.PriorityQueue()
    [await queue.put((random.randint(1, 6), "name{}".format(i))) for i in range(5)]
    tasks = [get_item(queue) for _ in range(5)]
    await asyncio.wait(tasks)
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

輸出結果如下:

get_item: (3, 'name4')
get_item: (4, 'name1')
get_item: (6, 'name0')
get_item: (6, 'name2')
get_item: (6, 'name3')

Queue常用方法如下:

  • empty(): 如果隊列爲空返回True,否則返回False
  • full(): 如果隊列中有maxsize個項目,則返回True,否則返回False
  • get(): 從隊列中刪除並返回一個項目,如果隊列爲空,則會阻塞直到有一個項目可用。需使用await阻塞
  • get_nowait(): 從隊列中刪除並返回一個項目,如果隊列爲空,則會引發QueueEmpty異常
  • join(): 阻塞直到隊列中的所有項目都已獲得並處理,需使用await阻塞
  • put(item): 將項目放入隊列,如果隊列已滿,則會阻塞直到有可用插槽爲止。需使用await阻塞
  • put_nowait(item): 將項目放入隊列而不會阻塞
  • qsize(): 返回隊列中的項目數
  • task_done(): 表示先前排隊的任務已完成
  • maxsize: 隊列中允許的項目數

信號量(Semaphore)

Semaphore(value=1, *, loop=None)通過併發量可以控制協程的併發數,爬蟲操作中使用該方法減少併發量,可以減少對服務器的壓力。請看如下代碼:

import time
import asyncio

async def run(sem, name):
    async with sem:
        print("{}進入run協程, {}".format(name, time.time()))
        await asyncio.sleep(1)
        
async def main():
    sem = asyncio.Semaphore(5)
    tasks = [run(sem, "name{}".format(i)) for i in range(18)]
    await asyncio.wait(tasks)

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

輸出結果如下:

name3進入run協程, 1585126139.9968233
name9進入run協程, 1585126139.9968233
name15進入run協程, 1585126139.9968233
name4進入run協程, 1585126139.9968233
name10進入run協程, 1585126139.9968233
name16進入run協程, 1585126140.9972842
name1進入run協程, 1585126140.9972842
name5進入run協程, 1585126140.9972842
name11進入run協程, 1585126140.9972842
name17進入run協程, 1585126140.9972842
name0進入run協程, 1585126141.9978023
name6進入run協程, 1585126141.9978023
name12進入run協程, 1585126141.9978023
name7進入run協程, 1585126141.9978023
name13進入run協程, 1585126141.9978023
name2進入run協程, 1585126142.9983144
name8進入run協程, 1585126142.9983144
name14進入run協程, 1585126142.9983144

從結果可以看到,併發量爲5!

常用方法如下:

  • locked(): 如果獲得了鎖,則返回True,否則返回False
  • acquire(): 獲取鎖。此方法將一直鎖定直到解鎖,將其設置爲locked,並返回True
  • release(): 釋放鎖。當鎖被鎖定,重置爲解鎖。無返回值

多線程與隊列相關知識:https://blog.csdn.net/y472360651/article/details/73495122

自此,Over~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章