主頁 > 後端開發 > Python異步并發機制詳解,讓你的代碼運行效率就像搭上了火箭!!!

Python異步并發機制詳解,讓你的代碼運行效率就像搭上了火箭!!!

2021-04-22 11:03:33 後端開發

在這里插入圖片描述

文章目錄

    • 探究低層建筑:asyncio
      • 同步/異步
      • 了解一下協程
      • 相對于執行緒,協程的優勢
      • 同步代碼轉異步代碼
      • 通過asyncio講解協程
      • 所以,代碼到底怎么寫?!!!
        • 協程可以做哪些事?
        • 定義協程函式:
        • 驗證某函式是否協程函式:
        • await是什么情況:
        • 運行協程:
        • 回呼
        • 多協程
        • 關倍訓圈


探究低層建筑:asyncio

Python由于全域鎖(GIL)的存在,一直無法發揮多核的優勢,其性能一直飽受詬病,
不過,在IO密集型的網路編程各種,異步處理比同步處理能夠提升非常之高的速度,
而相對于其他語言,Python還有一個很明顯的優勢,那就是它的庫很多啊!!!

Python3版本引入了async/await特性,其特點是:當執行程序中遇到IO請求的時候,可以將CPU資源出讓,運行其他的任務;待IO完成之后,繼續執行之前的任務,協程切換與執行緒切換比較類似,但協程切換更輕,不需要作業系統參與(沒有堆疊切換操作,也沒有用戶態與內核態切換),

同步/異步

在介紹協程之前,我還是再說一下同步和異步的概念,如果對這兩個概念都混淆不清的話,下面的更不用說了,

==同步:串行,異步:并行,==不要被字面意思所迷惑,

同步是指完成事務的邏輯,先執行第一個事務,如果阻塞了,會一直等待,直到這個事務完成,再執行第二個事務,順序執行,,,

異步是和同步相對的,異步是指在處理呼叫這個事務的之后,不會等待這個事務的處理結果,直接處理第二個事務去了,通過狀態、通知、回呼來通知呼叫者處理結果,


我再簡單的介紹一下協程:

了解一下協程

協程,英文Coroutines,是一種比執行緒更加輕量級的存在,正如一個行程可以擁有多個執行緒一樣,一個執行緒也可以擁有多個協程,
在這里插入圖片描述

子程式,或者稱為函式,在所有語言中都是層級呼叫,比如A呼叫B,B在執行程序中又呼叫了C,C執行完畢回傳,B執行完畢回傳,最后是A執行完畢,

所以子程式呼叫是通過堆疊實作的,一個執行緒就是執行一個子程式,

子程式呼叫總是一個入口,一次回傳,呼叫順序是明確的,而協程的呼叫和子程式不同,

協程看上去也是子程式,但執行程序中,在子程式內部可中斷,然后轉而執行別的子程式,在適當的時候再回傳來接著執行,

注意,在一個子程式中中斷,去執行其他子程式,不是函式呼叫,有點類似CPU的中斷,比如子程式A、B:

def A():
    print '1'
    print '2'
    print '3'

def B():
    print 'x'
    print 'y'
    print 'z'

假設由協程執行,在執行A的程序中,可以隨時中斷,去執行B,B也可能在執行程序中中斷再去執行A,結果可能是:

1 x 2 y 3 z

但是在A中是沒有呼叫B的,所以協程的呼叫比函式呼叫理解起來要難一些,


相對于執行緒,協程的優勢

最大的優勢就是協程極高的執行效率,因為子程式切換不是執行緒切換,而是由程式自身控制,因此,沒有執行緒切換的開銷,和多執行緒比,執行緒數量越多,協程的性能優勢就越明顯,

第二大優勢就是不需要多執行緒的鎖機制,因為只有一個執行緒,也不存在同時寫變數沖突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多執行緒高很多,

因為協程是一個執行緒執行,那怎么利用多核CPU呢?最簡單的方法是多行程+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的性能,


同步代碼轉異步代碼

以下為一段同步代碼:

import time

def hello():
    time.sleep(1)

def run():
    for i in range(5):
        hello()
        print('Hello World:%s' % time.time())  # 任何偉大的代碼都是從Hello World 開始的!

run()

以下是一段異步代碼:

import time
import asyncio

# 定義異步函式
async def hello():
    asyncio.sleep(1)
    print('Hello World:%s' % time.time())

def run():
    for i in range(5):
        loop.run_until_complete(hello())

loop = asyncio.get_event_loop()

run()

通過asyncio講解協程

  1. 通過async def來定義一個協程函式,通過await來執行一個協程物件,協程物件、協程函式的概念如下所示:
async def func_1(): # 1. 定義了一個協程函式
    pass
    
async def func_2(): # 2. 注意要在函式內部呼叫協程函式,自身也必須定義為協程
    # 3. func_1()呼叫產生了一個協程物件,通過await來執行這個協程,如果不加await,
    # 直接以func_1()方式呼叫,則func_1中代碼并不會執行,
    await func_1()  

async def 用來定義異步函式,其內部有異步操作,每個執行緒有一個事件回圈,主執行緒呼叫asyncio.get_event_loop()時會創建事件回圈,你需要把異步的任務丟給這個回圈的run_until_complete()方法,事件回圈會安排協同程式的執行,

  1. 一般情況下,無法在一個非協程函式中阻塞地呼叫另一個協程,但你可以通過asyncio.ensure_future()來異步執行這個協程:
import asyncio
async def fun_1(): # 1. 定義了一個協程函式
    pass
 
def bar():
    asyncio.ensure_future(fun_1()) # 這里fun_1()將會在某個時間執行,具體執行順序未知
    
    # 這里是阻塞執行fun_1(),但這種呼叫,只能在event loop進入回圈之前呼叫(loop.run_forever()),
    # 否則會拋例外
    asyncio.get_event_loop().run_until_complete(fun_1) 
    print("fun_1() is executed!")

在一些框架中,會將某些函式定義為協程(即通過async修飾),這些函式都是在某個地方通過create_task,或者ensure_future來進行調度的,

  1. 協程鎖:協程之間也可能會有資源共享沖突,要防止資源共享沖突產生的資料一致性問題,需要使用asyncio.Lock,asyncio.Lock也遵從背景關系管理協議,

  2. 協程睡眠:協程函式在執行中會占用本執行緒的全部CPU時間,除非遇到IO切換出去,因此,如果你在函式中使用sleep(),在多執行緒中,一個執行緒進入sleep狀態,作業系統會切換到其它執行緒執行,整個程式仍然是可回應的(除了該執行緒,它必須等待睡眠狀態結束);而對協程來說,同一loop中的其它協程都不會得到執行,因為這個sleep會占用本執行緒的全部執行時間,直到協程執行完畢,

上面的問題引出一個推論,也就是如果一個協程確實需要睡眠(比如某種定時任務),必須使用asyncio.sleep()

  1. 如果我們要通過asyncio來遠程呼叫一個服務,應該如何封裝呢?假設你使用的底層通訊的API是發送和接收分離的(一般比較靠近底層的API都是這樣設計的),那么你會面臨這樣的問題:當你通過異步請求(比如send)發出API request后,服務器的回應可能是通過on_message這樣的API來接收的,如何讓程式在呼叫send之后,就能得到(形式上)回傳結果,然后根據回傳結果繼續執行呢?
from typing import Dict
 
# 全域事件注冊表,鍵為外發請求的track_id,該track_id需要服務器在回應請求時傳回,
# 值為另一個dict,儲存著對應的asyncio.Event和網路請求的回傳結果,這里也可以使用list,
# 在強調性能的場合下,使用List[event: asyncio.Event, result: object]更好,
_events: Dict[str, Dict] = {}
 
# 定義阻塞呼叫的協程
async def sync_call(request):
  event = asyncio.Event()
  track_id = str(uuid.uuid4())
  _events[track_id] = {
    "events": event,
    "result": None
  }
   
  # 發送網路請求,以下僅為示例,具體網路請求要根據業務具體場景來替換,這一步一般是立即回傳,
  # 服務器并沒有來得及準備好response
  await aiohttp.request(...)
   
  # L1: 阻塞地等待事件結果,當框架(或者你的網路例程)收到服務器回傳結果時,根據track_id
  # 找到對應的event,觸發之
  await event.wait()
  
  # 獲取結果,并做清理
  response = _events[track_id].get("result")
  _events.pop(track_id)
 
  return response
 
# 在框架(或者你的網路例程)的訊息接收處,比如on_message函式體中:
async def on_message(response):
    # 如果服務器不傳回track_id,則整個機制無法生效
    track_id = response.get("track_id")
     
    waited = _events.get(track_id)
    if waited:
        waited["result"] = response
        waited["event"].set()   # !這里喚醒在L1處等待執行的

不能再深挖了,畢竟大家都是第一次接觸這個模塊兒,
必須要再深挖,這里面包含了太多的后端設計思想,是一個很重要的模塊兒,
但是不是在這篇里面深挖,過幾天會再出一篇關于asyncio的底層原理的博客,歡迎大家關注,


所以,代碼到底怎么寫?!!!

我相信,看了這么久,還是沒有幾個人知道這玩意兒到底要怎么寫代碼,
說實話,換我看了這么多我也不知道啊,

沒事兒啊,重在理解嘛,是吧,

在這里插入圖片描述

協程可以做哪些事?

* 等待一個 future 結束
* 等待另一個協程(產生一個結果,或引發一個例外)
* 產生一個結果給正在等它的協程
* 引發一個例外給正在等它的協程

定義協程函式:

async def do_some_work(x): pass

驗證某函式是否協程函式:

print(asyncio.iscoroutinefunction(do_some_work)) # True

await是什么情況:

async def do_some_work(x):
    print("Waiting " + str(x))
    await asyncio.sleep(x)

asyncio.sleep 也是一個協程,所以 await asyncio.sleep(x) 就是等待另一個協程,

看一下檔案解釋:

sleep(delay, result=None, *, loop=None)
Coroutine that completes after a given time (in seconds)

看不懂沒關系,我現在也不懂,誒,就是玩兒,


運行協程:

呼叫協程函式,協程并不會開始運行,只是回傳一個協程物件,還會引發一條警告,

要讓這個協程物件運行的話,有兩種方式:

* 在另一個已經運行的協程中用 `await` 等待它
* 通過 `ensure_future` 函式計劃它的執行

下面先拿到當前執行緒預設的 loop ,然后把協程物件交給 loop.run_until_complete,協程物件隨后會在 loop 里得到運行,

loop = asyncio.get_event_loop()
loop.run_until_complete(do_some_work(5))
# 上面這行代碼屬于簡寫,完整寫法是這樣的:
# loop.run_until_complete(asyncio.ensure_future(do_some_work(3)))
# run_until_complete 的引數是一個 future,它在內部會通過 ensure_future 函式把協程物件包裝成了 future,

接下來就比較抽象了,需要一定的基礎了,

回呼

假如協程是一個 IO 的讀操作,我們希望知道它什么時候結束運行,以便下一步資料的處理,這一需求可以通過往 future 添加回呼來實作,

def done_callback(futu):
    print('Done')

futu = asyncio.ensure_future(do_some_work(3))
futu.add_done_callback(done_callback)

loop.run_until_complete(futu)

多協程

為了把多個協程交給 loop,需要借助 asyncio.gather 函式,

方法一:

loop.run_until_complete(asyncio.gather(do_some_work(3), do_some_work(5)))

方法二:

先把協程存在串列里

coros = [do_some_work(1), do_some_work(3)]
loop.run_until_complete(asyncio.gather(*coros))

這兩個協程是并發運行的,所以等待的時間不是 1 + 3 = 4 秒,而是以耗時較長的那個協程為準,


關倍訓圈

loop 只要不關閉,就還可以再運行,但是如果關閉了,就不能再運行了,
建議呼叫 loop.close,以徹底清理 loop 物件防止誤用,


這一篇就先到這里啦,至于asyncio再往底層走,這周會更新的啦,能看到這里的小伙伴不容易,需要多大的毅力啊,
不準備收藏一下嗎?一次看這么多,怕是很難一次性消化掉吧,

在這里插入圖片描述

在這里插入圖片描述

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/278870.html

標籤:python

上一篇:python檔案名批量重命名腳本

下一篇:學習python 正則運算式——與你同行!

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more