1. 程式人生 > >深入理解協程(三):async/await實現非同步協程

深入理解協程(三):async/await實現非同步協程

原創不易,轉載請聯絡作者

深入理解協程分為三部分進行講解:

  • 協程的引入
  • yield from實現非同步協程
  • async/await實現非同步協程

本篇為深入理解協程系列文章的最後一篇。

從本篇你將瞭解到:

  1. async/await的使用。
  2. 如何從yield from風格的協程修改為async/await風格。

篇幅較長,請耐心閱讀。

async/await的引入

上篇【yield from實現非同步協程】我們引入了asynico模組,結合yield from實現非同步協程。但語法不夠簡潔,其中涉及的生成器,裝飾器也讓人頭疼不已。

為了語法更加簡潔。於是,在Python3.5(PEP 492)中新增了async/await

語法來實現非同步協程。

async/await的使用

先介紹幾個概念:

  • async/await :python3.5之後用於定義協程的關鍵字,async定義一個協程,await用於掛起阻塞的非同步呼叫介面。
  • event_loop :事件迴圈,程式開啟一個無限的迴圈,程式設計師會把一些函式註冊到事件迴圈上。當滿足事件發生的時候,呼叫相應的協程函式。
  • coroutine :協程物件,指一個使用async關鍵字定義的函式,它的呼叫不會立即執行函式,而是會返回一個協程物件。協程物件需要註冊到事件迴圈,由事件迴圈呼叫。
  • task :任務,是對協程進一步封裝,其中包含任務的各種狀態。
  • future: 代表將來執行或沒有執行的任務的結果。它和task上沒有本質的區別

1.建立協程

在def前加上async的宣告,就完成了一個協程函式的定義。協程函式不能直接呼叫執行,需要將協程註冊到事件迴圈,並啟動事件迴圈才能使用。

import asyncio

async def fun(a):    # 定義協程函式
    print(a)

# 呼叫協程函式,生成一個協程物件,此時協程函式並未執行
coroutine = fun('hello world')
# 建立事件迴圈
loop = asyncio.get_event_loop()
# 將協程函式新增到事件迴圈,並啟動
loop.run_until_complete(coroutine)

# 輸出
hello word

2. 任務物件task

協程物件不能直接執行,在註冊事件迴圈的時候,其實是run_until_complete方法將協程包裝成為了一個任務(task)物件。我們也可以顯式實現它。

實現方式1:

使用create_task()建立task。

import asyncio

async def fun(a):
    print(a)
    return a

coroutine = fun('hello world')
loop = asyncio.get_event_loop()
# 使用create_task()建立task,並將coroutine物件轉化成task物件
task = loop.create_task(coroutine)
print(f'task: {task}')
loop.run_until_complete(task)
print(f'task: {task}')
print(f'result: {result}')

輸出結果:

task: <Task pending coro=<fun() running at D:/test.py:3>>
hello world
task: <Task finished coro=<fun() done, defined at D:/test.py:3> result='hello world'>

從輸出結果能看到,建立task物件後,未將task新增到事件迴圈之前,狀態是pending;task物件執行完畢後,狀態是finished,並將引數a的值返回。

實現方式2:

使用asyncio 的 ensure_future() 方法,建立task。

import asyncio

async def fun(a):
    print(a)
    return a

coroutine = fun('hello world')
# 使用asyncio 的 ensure_future() 方法,建立task,並將coroutine物件轉化成task物件
task = asyncio.ensure_future(coroutine)
loop = asyncio.get_event_loop()
print(f'task: {task}')
loop.run_until_complete(task)
print(f'task: {task}')

輸出結果:

task: <Task pending coro=<fun() running at D:/test.py:3>>
hello world
task: <Task finished coro=<fun() done, defined at D:/test.py:3> result='hello world'>

通過ensure_future() 可以在loop未定義前建立task。實現效果與上面相同。

3.繫結回撥函式

如果需要在task執行完畢後對結果進行處理,可以通過給task繫結回撥函式完成,回撥的最後一個引數是future物件(如task物件)。

import asyncio

async def fun(a):
    print(a)
    return a

def callback(task): # 回撥函式,列印task的返回值
    print(f'result: {task.result()}')

coroutine = fun('hello world')
loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)
task.add_done_callback(callback)    #繫結回撥函式
print(f'task: {task}')
loop.run_until_complete(task)
print(f'task: {task}')

輸出結果:

task: <Task pending coro=<fun() running at D:/test.py:3> cb=[callback() at D:/Study/Python/python_text/非專案/協程.py:7]>
hello world
result: hello world # 完成了返回值的列印
task: <Task finished coro=<fun() done, defined at D:/test.py:3> result='hello world'>

4.多工協程

如果我們需要執行多個任務時,我們可以定義一個任務列表,並將需要完成的協程任務都加進去。將原本的loop.run_until_complete(tasks)改為loop.run_until_complete(asyncio.wait(tasks))

如果執行的是多個耗時的任務,如網路請求、檔案讀取等。此時就await就派上用場了,await可以針對耗時的操作進行掛起,就像生成器裡的yield一樣,函式讓出控制權。協程遇到await,事件迴圈將會掛起該協程,執行別的協程,直到其他的協程也掛起或者執行完畢,再進行下一個協程的執行。

舉個例子:

import time
import asyncio

async def taskIO_1():   
    print('開始執行IO任務1...')
    await asyncio.sleep(2)  
    print('IO任務1已完成,耗時2s')
    return taskIO_1.__name__

async def taskIO_2():       
    print('開始執行IO任務2...')
    await asyncio.sleep(3)  
    print('IO任務2已完成,耗時3s')
    return taskIO_2.__name__

if __name__ == '__main__':
    start = time.time()
    loop = asyncio.get_event_loop() 
    tasks = [taskIO_1(), taskIO_2()]
    loop.run_until_complete(asyncio.wait(tasks)) # 完成事件迴圈,直到最後一個任務結束
    print('所有IO任務總耗時%.5f秒' % float(time.time()-start))
    
# 輸出
開始執行IO任務2...
開始執行IO任務1...
IO任務1已完成,耗時2s
IO任務2已完成,耗時3s
所有IO任務總耗時3.00251秒

可以看出,原本需要5秒,現在執行只需要3秒。

yield from轉async/await

上述程式碼有沒有很眼熟。

其實,這段程式碼正是【yield from實現非同步協程】末尾,yield from結合asynico實現非同步協程的程式碼。只是將yielf from風格變為async/await風格。

由於async/awaityield from風格的協程底層實現方式相同。因此,從yield from風格改為async/await風格非常容易。只需:

  • @asyncio.coroutine替換為async
  • yield from替換為await

async/await風格的程式碼隱藏了裝飾器、yield from語法,方便了人們的理解,同時也讓程式碼更加簡潔。

推薦閱讀

深入理解協程(一):協程的引入

深入理解協程(二):yield from實現非同步協程

年底送福利,關注公眾號【西加加先生】,2020一起玩Python,公眾號回覆【2020】即可獲取抽獎方式參與抽獎。

相關推薦

深入理解async/await實現非同步

原創不易,轉載請聯絡作者 深入理解協程分為三部分進行講解: 協程的引入 yield from實現非同步協程 async/await實現非同步協程 本篇為深入理解協程系列文章的最後一篇。 從本篇你將瞭解到: async/await的使用。 如何從yield from風格的協程修改為async/aw

深入理解yield from實現非同步

原創不易,轉載請聯絡作者 深入理解協程分為三部分進行講解: 協程的引入 yield from實現非同步協程 async/await實現非同步協程 本篇為深入理解協程系列文章的第二篇。 yield from yield from是Python3.3(PEP 380)引入的新語法。主要用於解決在生成器

深入理解async/await非同步爬蟲實戰

本文目錄: 同步方式爬取部落格標題 async/await非同步爬取部落格標題 本片為深入理解協程系列文章的補充。 你將會在從本文中瞭解到:async/await如何運用的實際的爬蟲中。 案例 從CSDN上批量爬取指定文章的標題。文章列表如下: urls = [ 'https://blog.csd

並發編從AQS到CountDownLatch與ReentrantLock

splay public 繼續 for admin font 通信 html integer 一、目錄 1、AQS簡要分析 2、談CountDownLatch 3、談ReentrantLock 4、談消費者與生產者模式(not

深入理解JavaScript系列16閉包Closures

ava hive auto flow style this quest 情況 知識 介紹 本章我們將介紹在JavaScript裏大家常常來討論的話題 —— 閉包(closure)。閉包事實上大家都已經談爛了。雖然如此,這裏還是要試著從理論角度來討論下閉包,

[轉]深入理解閉包

copy AI strong 查找 cte 分組操作 spa 方法 詳細介紹 嚴格來講,IIFE並不是閉包,因為它並不滿足函數成為閉包的三個條件。但一般地,人們認為IIFE就是閉包,畢竟閉包有多個定義。本文將詳細介紹IIFE的實現和用途 實現   函數跟隨一對圓括號()

多線檢索線對象

rgs main 停止 單獨 () read div 替代 主線程 class Program9 { //檢索線程對象 //停止所有前臺線程後,運行時將停止所有後臺線程,並關閉。 static Object obj

深入理解設計模式12職責鏈模式

一、什麼是職責鏈模式 客戶端發出一個請求,鏈上的物件都有機會來處理這一請求,而客戶端不需要知道誰是具體的處理物件。這樣就實現了請求者和接受者之間的解耦,並且在客戶端可以實現動態的組合職責鏈。使程式設計更有靈活性。 定義:使多個物件都有機會處理請求,從而避免了請求的傳送者和接受者之間的耦合關係。將這些物件連

深入理解JavaScript系列5強大的原型和原型鏈

JavaScript 不包含傳統的類繼承模型,而是使用 prototypal 原型模型。 雖然這經常被當作是 JavaScript 的缺點被提及,其實基於原型的繼承模型比傳統的類繼承還要強大。實現傳統的類繼承模型是很簡單,但是實現 JavaScript 中的原型繼承則要困難的多。 &l

深入理解JavaScript系列2揭祕命名函式表示式 命名函式表示式 函式表示式 函式宣告

還有一種函式表示式不太常見,就是被括號括住的(function foo(){}),他是表示式的原因是因為括號 ()是一個分組操作符,它的內部只能包含表示式,我們來看幾個例子: 函式宣告只能出現在程式或函式體內。 如果function foo(){}是作為賦值表示式的一部分的

深入理解設計模式13直譯器模式

一、什麼是直譯器模式 定義:給定一個語言,定義一個文法的一種表示, 並定義一個直譯器, 這個直譯器使用該表示來解釋語言中的句子。   直譯器模式所涉及的角色如下所示:   (1)抽象表示式(Expression)角色:宣告一個所有的具體表達式角色都需要實現的抽象介面。這個介面主要是一個i

Java——深入理解Class物件Class物件的載入及其獲取方式

上一篇部落格Java——深入理解Class物件(一)帶大家簡單認識了一下Java中Class物件。 現在帶大家瞭解一下Class物件的載入及其獲取方式。 1.Class物件的載入 在Java——深入理解Class物件(一)我們已提到過,Class物件是由JVM載入的,那它必然不會是胡亂載

Java——深入理解Class物件什麼是Class物件

Class類是我們再熟悉不過的東西,但是對於Class物件,很多人卻是一臉懵逼。 Class物件到底是什麼呢?今天我們就來深入瞭解一下它。 1.RTTI的概念 RTTI(Run-Time Type Identification),即執行時型別識別,這個詞一直是 C++ 中的概念,至

深入理解 Laravel Eloquent——模型刪除及軟刪除相關實現

1、刪除模型 1.1 使用delete刪除模型 刪除模型很簡單,先獲取要刪除的模型例項,然後呼叫delete方法即可: $post = Post::find(5); if($post->delete()){ echo '刪除文章成功!'; }else{ echo '

深入理解JavaScript系列2揭祕命名函式表示式

還有一種函式表示式不太常見,就是被括號括住的(function foo(){}),他是表示式的原因是因為括號 ()是一個分組操作符,它的內部只能包含表示式,我們來看幾個例子: 函式宣告只能出現在程式或函式體內。 如果function foo(

深入理解設計模式15訪問者模式

一、什麼是訪問者模式 定義:表示一個作用於其物件結構中的各元素的操作,它使你可以在不改變各元素類的前提下定義作用於這些元素的新操作。 可以對定義這麼理解:有這麼一個操作,它是作用於一些元素之上的,而這些元素屬於某一個物件結構。同時這個操作是在不改變各元素類的前提下,在這個前提下定義新操作是訪問者模式精髓中

深入理解JavaScript系列4立即呼叫的函式表示式

javascript 函式function前面的一元操作符, 感嘆號、小括號、一元操作符!()+-|| 看下面內容之前可以先看看上面的文章,總結的非常贊 前言 大家學JavaScript的時候,經常遇到自執行匿名函式的程式碼,今天我們主要就來想想說一下自執行 在詳細

深入理解阻塞佇列——LinkedBlockingQueue原始碼分析

LinkedBlockingQueue是一個基於連結串列實現的可選容量的阻塞佇列。隊頭的元素是插入時間最長的,隊尾的元素是最新插入的。新的元素將會被插入到佇列的尾部。 LinkedBlockingQueue的容量限制是可選的,如果在初始化時沒有指定容量,那麼預

深入理解JavaScript系列15函式Functions

  詳情請檢視:https://www.cnblogs.com/TomXu/archive/2012/01/30/2326372.html   本章節我們要著重介紹的是一個非常常見的ECMAScript物件——函式(function),我們將詳細講解一下各種型別的函式

深入理解計算機系統筆記連結

理解連結有很多好處: 有助於構造大型程式有助於避免一些危險程式設計錯誤有助於理解其他重要的系統概念讓你能夠利用共享庫1. 編譯器驅動程式 編譯命令,假設有main.c和swap.c兩個原始檔 $ gcc -O2 -g -o p main.c swap.c 實際上編譯過程