二十五、深入Python中的協程

@Author: Runsen

一說並發,你肯定想到了多線程+進程模型,確實,多線程+進程,正是解決並發問題的經典模型之一。但對於多核CPU,利用多進程+協程的方式,能充分利用CPU,獲得極高的性能。協程也是實現並發編程的一種方式。

協程

協程:是單線程下的並發,又稱微線程。英文名是Coroutine。它和線程一樣可以調度,但是不同的是線程的啟動和調度需要通過操作系統來處理。

協程是一種比線程更加輕量級的存在,最重要的是,協程不被操作系統內核管理,協程是完全由程序控制的。

運行效率極高,協程的切換完全由程序控制,不像線程切換需要花費操作系統的開銷,線程數量越多,協程的優勢就越明顯。

協程不需要多線程的鎖機制,因為只有一個線程,不存在變量衝突。

對於多核CPU,利用多進程+協程的方式,能充分利用CPU,獲得極高的性能。

注意協程這個概念完全是程序員自己想出來的東西,它對於操作系統來說根本不存在。操作系統只有進程和線程。

Python中使用協程的例子

yield關鍵字相當於是暫停功能,程序運行到yield停止,send函數可以傳參給生成器函數,參數賦值給yield

def customer():
    while True:
        number = yield
        print('開始消費:',number)
        
custom = customer()
next(custom)
for i in range(5):
    print('開始生產:',i)
    custom.send(i)

結果如下

開始生產: 0
開始消費: 0
開始生產: 1
開始消費: 1
開始生產: 2
開始消費: 2
開始生產: 3
開始消費: 3
開始生產: 4
開始消費: 4

代碼解析:

  1. 協程使用生成器函數定義:定義體中有 yield 關鍵字。
  2. yield 在表達式中使用;如果協程只需從客戶custom接收數據,如果沒有產出的值,那麼產出的值是 None。
  3. 首先要調用 next(…) 函數,因為生成器還沒啟動,沒在 yield 語句處暫停,所以一開始無法發送數據。
  4. 調用send方法,把值傳給 yield 的變量,然後協程恢復,繼續執行下面的代碼,直到運行到下一個 yield 表達式,或者終止。

async和await

async和await是原生協程,是Python3.5以後引入的兩個關鍵詞。

async 是「異步」的簡寫,而 await 可以認為是 async wait 的簡寫。所以應該很好理解 async 用於申明一個 function 是異步的,而 await 用於等待一個異步方法執行完成。

下面,我們從一個demo示例看起,具體代碼如下。

import time 

def print_num(num):
    print("Maoli is printing " + str(num) + " nows" )
    time.sleep(1)
    print("Maoli prints" + str(num) + " OK")

def main(nums):
    for num in nums:
        print_num(num)
%time main([i for i in range(1,6)])


Maoli is printing 1 nows
Maoli prints1 OK
Maoli is printing 2 nows
Maoli prints2 OK
Maoli is printing 3 nows
Maoli prints3 OK
Maoli is printing 4 nows
Maoli prints4 OK
Maoli is printing 5 nows
Maoli prints5 OK
Wall time: 5 s

%time 需要在jupyter notebook中運行,這是jupyter的語法糖。

上面代碼是從上到下執行的。下面我們將上面的代碼改成單線程協程版本。

注意py版本3.7以上,主要使用的是asyncio模塊,如果出現AttributeError: module 『asyncio『 has no attribute 『run『報錯,這是asyncio版本不兼容的原因,需要將Python版本提升至3.7以上。

import asyncio

async def print_num(num):
    print("Maoli is printing " + str(num) + " nows" )
    await asyncio.sleep(1)
    print("Maoli prints" + str(num) + " OK")

async def main(nums):
    for num in nums:
        await print_num(num)
%time asyncio.run(main([i for i in range(1,6)]))


Maoli is printing 1 nows
Maoli prints1 OK
Maoli is printing 2 nows
Maoli prints2 OK
Maoli is printing 3 nows
Maoli prints3 OK
Maoli is printing 4 nows
Maoli prints4 OK
Maoli is printing 5 nows
Maoli prints5 OK
Wall time: 5.01 s

asyncio.run() 函數用來運行最高層級的入口點 “main()” 函數。await 是同步調用等待一個協程。以下代碼段會在等待 1 秒後打印 num,但在運行速度上沒有發生改變。這裡需要引入asyncio.create_task可等待對象才可以。

create_task

如果一個對象可以在 await 語句中使用,那麼它就是可等待對象。

協程中的還一個重要概念,任務(Task)。

如果寫一個數字是一個任務,那麼毛利我要完成5個任務。

毛利我寫個1-5都這麼慢,不行,我要加速寫。

asyncio.create_task() 函數用來並發運行作為 asyncio 任務 的多個協程。

import asyncio

async def print_num(num):
    print("Maoli is printing " + str(num) + " nows" )
    await asyncio.sleep(1)
    print("Maoli prints" + str(num) + " OK")

async def main(nums):
    tasks = [asyncio.create_task(print_num(num)) for num in nums]
    for task in tasks:
        await task
%time asyncio.run(main([i for i in range(1,6)]))


Maoli is printing 1 nows
Maoli is printing 2 nows
Maoli is printing 3 nows
Maoli is printing 4 nows
Maoli is printing 5 nows
Maoli prints1 OK
Maoli prints3 OK
Maoli prints5 OK
Maoli prints2 OK
Maoli prints4 OK
Wall time: 1.01 s

還可以寫成await asyncio.gather(*tasks)這種方法

import asyncio

async def print_num(num):
    print("Maoli is printing " + str(num) + " nows" )
    await asyncio.sleep(1)
    print("Maoli prints" + str(num) + " OK")

async def main(nums):
    tasks = [asyncio.create_task(print_num(num)) for num in nums]
    await asyncio.gather(*tasks)
%time asyncio.run(main([i for i in range(1,6)]))

*tasks 解包列表,將列表變成了函數的參數;與之對應的是, ** dict 將字典變成了函數的參數。

協程的寫法簡潔清晰,只要把 async / await 語法和 create_task 結合來用,就是Python中比較常見的協程寫法。

今天也學到了很多東西呢,明天有什麼新知識呢?真期待鴨如果喜歡文章可以關注我哦

//docs.python.org/zh-cn/3/library/asyncio.html

本文已收錄 GitHub,傳送門~ ,裏面更有大廠面試完整考點,歡迎 Star。

Tags: