非同步編程指北

来源:https://www.cnblogs.com/88223100/archive/2022/08/22/Asynchronous-Programming-Guide-North.html
-Advertisement-
Play Games

同步、非同步,併發、並行、串列,這些名詞在我們的開發中會經常遇到,這裡對非同步編程做一個詳細的歸納總結,希望可以對這方面的開發有一些幫助。 ...


同步、非同步,併發、並行、串列,這些名詞在我們的開發中會經常遇到,這裡對非同步編程做一個詳細的歸納總結,希望可以對這方面的開發有一些幫助。

  

 

 

1 幾個名詞的概念

多任務的時候,才會遇到的情況,如:同步、非同步,併發、並行。

1.1 理清它們的基本概念

併發:多個任務在同一個時間段內同時執行,如果是單核心電腦,CPU 會不斷地切換任務來完成併發操作。

並行:多任務在同一個時刻同時執行,電腦需要有多核心,每個核心獨立執行一個任務,多個任務同時執行,不需要切換。

同步:多任務開始執行,任務 A、B、C 全部執行完成後才算是結束。

非同步:多任務開始執行,只需要主任務 A 執行完成就算結束,主任務執行的時候,可以同時執行非同步任務 B、C,主任務 A 可以不需要等待非同步任務 B、C 的結果。

併發、並行,是邏輯結構的設計模式。

同步、非同步,是邏輯調用方式。

串列是同步的一種實現,就是沒有併發,所有任務一個一個執行完成。

併發、並行是非同步的 2 種實現方式。

1.2 舉一個例子

 

 

你的朋友在廣州,但是有 2 輛小汽車在深圳,需要你幫忙把這 2 輛小汽車送到廣州去。

同步的方式,你先開一輛小汽車到廣州,然後再坐火車回深圳,再開另外一輛小汽車去廣州。這是串列的方法,2 輛車需要的時間也就更長了。

非同步的方式,你開一輛小汽車從深圳去廣州,同時請一個代駕把另外一輛小汽車從深圳開去廣州。這也就是並行方法,兩個人兩輛車,可以同時行駛,速度很快。

併發的方式,你一個人,先開一輛車走 500 米,停車跑回來,再開另外一輛車前行 1000 米,停車再跑回來,迴圈從深圳往廣州開。併發的方式,你可以把 2 輛車一塊送到朋友手裡,但是過程還是很辛苦的。

1.3 思考問題

你找一家汽車托運公司,把 2 輛車一起托運到廣州。這種方式是同步、非同步,併發、並行的哪種情況呢?

 

2 併發/並行執行會遇到的問題

2.1 問題 1:併發的任務數量控制

 

 

假設:某個介面的併發請求會達到 1 萬的 qps,所以對介面的性能、響應時長都要求很高。

介面內部又有大量 redis、mysql 數據讀寫,程式中還有很多處理邏輯。如果介面內的所有邏輯處理、數據調用都是串列化,那麼單個請求耗時可能會超過 100ms,為了性能優化,就會把數據讀取的部分與邏輯計算的部分分開來考慮和實現,能夠獨立的部分單獨剝離出來作為非同步任務來執行,這樣就把串列化的耗時優化為併發執行,充分利用多核電腦的性能,減少單個介面請求的耗時。

假設的數據具體化,如:這個介面的數據全部是可以獨立獲取(支持併發),需要讀取來自不同數據結構的 redis 共 10 個,讀取不同數據表的數據共 10 個。那麼一次請求,數據獲取就會啟動 10 個 redis 讀取任務,10 個 mysql 讀取任務。每秒鐘 1 萬介面請求,會有 10 萬個 redis 讀取任務和 10 萬個 mysql 讀取任務。這 21 萬的併發任務,在一秒鐘內由 16/32 核的後端部署單機來完成,雖然在同一時刻的任務數量不一定會是 21 萬(速度快的話會少於 21 萬,如果處理速度慢,出現請求積壓擁堵,會超過 21 萬)。

這時候,會遇到的瓶頸。

記憶體,如果每個任務需要 500k 記憶體,那麼 210k*0.5M=210*0.5G=105G.

CPU,任務調度,像 golang 的協程可能開銷還小一些,如果是 java 的線程調度,操作系統會因為調度而空轉。

網路,每次數據讀取 5k,那麼 200k5k=2005M=1G.

埠,埠號最多能分配出來 65536 個,明顯不夠用了。

數據源,redis 可以支持 10 萬 qps 的請求,但是 mysql 就難以支持 10 萬 qps 了。

上面可能出現的瓶頸中,通過電腦資源擴容可以解決大部分問題,比如:部署 50 個後端實例,每個實例只需要應對 200 的 qps,壓力就小了很多。對於數據源,mysql 可以有多個 slave 來支持只讀的請求。

但是,如果介面的併發量更大呢?或者某個/某些數據源讀取出現異常,需要重試,或者出現擁堵,介面響應變慢,任務數量也就會出現暴增,後端服務的各方面瓶頸又會隨之出現。

所以,我們需要特別註意和關心後端開啟的非同步任務數量,要做好異常情況的防範,及時中斷掉擁堵/超時的任務,避免任務暴增導致整個服務不可用

2.2 思考問題

你要如何應對這類併發任務暴增的情況呢?如何提前預防?如何及時干預呢?

2.3 問題 2:共用數據的讀寫順序和依賴關係

共用數據的併發讀寫,是併發編程中的老大難問題,如:讀寫臟數據,舊數據覆蓋新數據等等。

而數據的依賴關係,也就決定了任務的執行先後順序。

為了避免共用數據的競爭讀寫,為了保證任務的先後關係,就需要用到鎖、隊列等手段,這時候,併發的過程又被部分的拉平為串列化執行。

2.4 舉個例子

 

 

https://www.ticketmaster.com/eastern-conf-semis-tbd-at-boston-boston-massachusetts/event/01005C6AA5531A90

NBA 季後賽,去現場看球,要搶購球票,體育館最多容納 1 萬人(1 萬張球票)。

體育館不同距離、不同位置的票,價格和優惠都不相同。有單人位、有雙人位,也有 3、4 人位。你約著朋友共 10 個人去看球,要買票,要選位置。這時候搶票就會很尷尬,因為位置連著的可能會被別人搶走,同時買的票越多,與人衝突的概率就越大,會導致搶票特別困難。

同時,這個系統的開發也很頭大,搶購(秒殺)的併發非常大,預計在開始的一秒鐘會超過 10 萬人同時進來,再加上刷票的機器人,介面請求量可能瞬間達到 100 萬的 QPS。

較簡單的實現方式,所有的請求都非同步執行,訂單全部進入消息隊列,下單馬上響應處理中,請等待。然後,後端程式再從消息隊列中串列化處理每一個訂單,把出現衝突的訂單直接報錯,這樣,估計 1 秒鐘可以處理 1000 個訂單,10 秒鐘可以處理 1 萬個訂單。考慮訂單的衝突問題,1 萬張球票的 9000 張可能在 30 秒內賣出去,此時只處理了 3 萬個訂單,第一秒鐘進來的 100 萬訂單已經在消息隊列中堆積,又有 30 秒鐘的新訂單進來,需要很久才可以把剩下的 1000 張球票賣出去啊。同理,下單的用戶需要等待太久才知道自己的訂單結果,這個過程輪詢的請求也會很多很多。

換一種方案,不使用隊列串列化處理訂單,直接併發的處理每一個訂單。那麼處理流程中的數據都需要梳理清楚。

1 針對每一個用戶的請求加鎖,避免同一個用戶的重入;

2 每一個/組座位預生成一個 key:0,預設 0 說明沒有下單;

3 預估平均每一個訂單包含 2 個/組座位,需要更新 2 個座位 key;

4 下單的時候給座位 key 執行 INCR key 數字遞增操作,只有返回 1 的訂單才是成功,其他都是失敗;

5 如果同一個訂單中的座位 key 有衝突的情況下,需要回滾成功 key(INCR key = 1)重置(SET key 0);

6 訂單成功/失敗,處理完成後,去掉用戶的請求鎖;

7 訂單數據入庫到 mysql(消息隊列,避免 mysql 成為瓶頸);

綜上,需要用到 1 個鎖(2 次操作),平均 2 個座位 key(每個座位號 1-2 次操作),這裡只有 2 個座位 key 可以併發更新。為了讓 redis 不成為數據讀寫的瓶頸(超過 100w 的 QPS 寫操作),不能使用單實例模式,而要使用 redis 集群,使用由 10-20 個 redis 實例組成的集群,來支持這麼高的 redis 數據讀寫。

算上 redis 數據讀寫、參數、異常、邏輯處理,一個請求大概耗時 10ms 左右,單核至少可以支持 100 併發,由於這裡有大量 IO 處理,後端服務可以支持的併發可以更高些,預計單核 200 併發,16 核就可以支持 3200 併發。總共需要支持 100 萬併發,預計需要 312 台後端伺服器。

這種方案比隊列的方案需要的伺服器資源更多,但是用戶的等待時間很短,體驗就好很多。

2.5 思考問題

實際情況會是怎樣呢?會有 10 萬人同時搶票嗎?會有 100 萬的超高併發嗎?訂票系統真的會準備 300 多台伺服器來應對搶票嗎?

 

3 狀態處理:忽略結果

 

 

3.1 使用場景和案例

使用場景,主流程之外的非同步任務,可能重要程度不高,或者處理的複雜度太高,有時候會忽略非同步任務的處理結果。

案例 1:非同步的數據上報、數據存儲/計算/統計/分析。

案例 2:模板化創建服務,有很多個任務,有前後關聯任務,也有相互獨立任務,有些執行速度很慢,有些任務失敗後也可以手動重試來修複。

忽略結果的情況,就會遇到下麵的問題。

3.2 問題 1:數據一致性

看下案例 1 的情況。

非同步的日誌上報,是否成功發送到服務端呢?

非同步的指標數據上報,是否正確彙總統計和發送到服務端呢?

非同步的任務,數據發送到消息隊列,是否被後端應用程式消費呢?

服務端是否正常存儲和處理完成呢?

如果因為網路原因,因為併發量太大導致服務負載問題,因為程式 bug 的原因,導致數據沒能正確上報和處理,這時候的數據不一致、丟失的問題,就會難以及時排查和事後補發。

如果在本地完整記錄一份數據,以備數據審查,又要考慮高併發高性能的瓶頸,畢竟本地日誌讀寫性能受到磁碟速度的影響,性能會很差。

3.3 問題 2:功能可靠性

看下案例 2 的情況。

創建服務的過程中,有創建代碼倉庫、開啟日誌採集和自定義鏡像中心,CI/CD 等耗時很長的任務。這裡開啟日誌採集和自定義鏡像中心如果出現異常,對整個服務的運行沒有影響,而且開發者發現問題後也可以自己手動操作下,再次開啟日誌採集和自定義鏡像功能。所以在模板化處理中,這些非同步處理任務就沒有關註任務的狀態。

那麼問題就很明顯,模板化創建服務的過程中,是不能保證全部功能都正常執行完成的,會有部分功能可能有異常,而且也沒有提示和後續指引。

當然模板化創建服務的程式,也可以把全部任務的狀態都檢查結果,只是會增加一些處理的複雜度和難度。

3.4 思考問題

實際開發中,有遇到類似上面的兩個案例嗎?你會如何處理呢?所有的非同步任務,都會檢查狀態結果嗎?為什麼呢?

 

4 狀態處理:結果返回

4.1 使用場景和案例

大部分的非同步任務對於狀態結果還是很關註的,比如:後續的處理邏輯或者任務依賴某個非同步任務,或者非同步任務非常重要,需要把結果返回給請求方。

案例 1:模板化創建服務的過程中,需要非同步創建服務的 git 代碼倉庫,還要給倉庫添加成員、webhook、初始化代碼等。整個過程全部串列化作為一個任務的話,耗時會比較長。可以把創建服務的 git 代碼倉庫作為一個非同步任務,然後得到成功的結果後再非同步的發起添加成員、加 webhook、初始化代碼等任務。同時,這裡的 CI/CD 有配置相關,有執行相關,整個過程也很長,CD 部署成功之後才可以開啟日誌採集等配置,所以也需要關註 CD 部署的結果。

案例 2:各種 webhook、callback 介面和方法,就是基於回調的方式,如:golang 中的 channel 通知,工蜂中的代碼 push 等 webhook,監控告警中的 callback 等。

案例 3:發佈訂閱模式,如引入消息隊列服務,主程式把數據發送給消息隊列,非同步任務訂閱相應的主題然後處理。處理完成後也可以把結果再發送給消息隊列,或者把結果發送給主調程式的介面,或者等待主調程式來查詢結果,當然也可能是上面的忽略結果的情況。

從上可以總結出來,對於非同步任務的狀態處理,需要關註結果的話,有兩種主要的方法,分別是:輪詢查詢和等待回調。

4.2 方法 1:輪詢查詢

 

 

上面的案例 1 中,模板化創建服務的過程很慢,所以整個功能都是非同步的,用戶大概要等待 10s 左右才知道最後的結果。所以,用戶在創建服務之後,瀏覽器會不斷輪詢服務端介面,看看創建服務的結果,各個步驟的處理結果,服務配置是否都成功完成了。

類似的功能實現應該有很多,比如:服務構建、部署、創建鏡像倉庫、搶購買票等,把任務執行和任務結果通過非同步的方式強制分離開,用戶可以等待,但是不用停留在當前任務中持續等待,而是可以去做別的事情,隨時回來關註下這個任務的處理結果就好了。大部分執行時間很長的任務都會放到非同步線程中執行,用戶關註結果的話,就可以通過查詢的方式來獲取結果,程式自動來返回結果的話,就可以用到輪詢查詢了。

局限性 1:頻率和實時性

輪詢的方式延時可能會比較高,因為跟定時器的間隔時間有關係。

局限性 2:增加請求壓力

因為輪詢,要不斷地請求服務端,所以對後端的請求壓力也會比較大。

4.3 方法 2:通知回調

 

 

等待回調幾乎是實時的,處理有結果返回就馬上通過回調通知到主程式/用戶,那麼效率和體驗上就會好很多。

但是這裡也有一個前提要求,回調的時候,主程式必須還在運行,否則回調也就沒有了主體,也就無效了。所以要求主程式需要持續等待非同步任務的回調,不能過早的退出。

一般程式中使用非同步任務,需要得到任務狀態的結果,使用等待回調的情況更多一些。

特別註意 1:等待超時

等待的時間,一般不能是無限長,這樣容易造成某些異常情況下的任務爆炸,記憶體泄露。所以需要對非同步任務設置一個等待超時,過期後就要中斷任務了,也就不能通過回調來得到結果了,直接認為是任務異常了。

特別註意 2:異常情況

當主程式在等待非同步任務的回調時,如果非同步任務自身有異常,無法成功執行,也無法完成回調的操作,那麼主程式也就無法得到想要的結果,也不知道任務狀態的結果是成功還是失敗,這時候也就會遇到上面等待超時的情況了。

特別註意 3:回調地獄

使用 nodejs 非同步編程的時候,所有的 io 操作都是非同步回調,於是就很容易陷入 N 層的回調,代碼就會變得異常醜陋和難以維護。於是就出現了很多的非同步編程框架/模式,像:Promise,Generator,async/await 等。這裡不做過多講解。

4.4 思考問題

實際工作中,還有哪些地方需要處理非同步任務的狀態結果返回呢?除了輪詢和回調,還有其他的方法嗎?

 

5 異常處理

同步的程式,處理異常情況,在 java 中只需要一個 try catch 就可以捕獲到全部的異常。

5.1 重點 1:分別做異常處理

非同步的程式,try catch 只能捕獲到當前主程式的異常,主程式中的非同步線程是無法被捕獲的。這時候,就需要針對非同步線程中的非同步任務也要單獨進行 try catch 捕獲異常。

在 golang 中,開啟協程,還是需要在非同步任務的 defer 方法中,加入一個 recover() ,以避免沒有處理的異常導致整個進程的 panic。

5.2 重點 2:異常結果的記錄,查詢或者回調

當我們把非同步任務中的異常情況都處理好了,不會導致非同步線程把整個進程整奔潰了,那麼還有問題,怎麼把異常的結果返回給主進程。這就涉及到上面的狀態處理了。

如果可以忽略結果,那麼只需要寫一下錯誤日誌就好了。

如果需要處理狀態,那就要記錄下異常信息或者通知回調給到主進程。

5.3 思考問題

實際工作中,你會對所有的可能異常情況都做相應的處理嗎?異常結果,都是怎麼處理的呢?

 

6 典型場景和思考

前面已經講到一些案例,總結下來的典型場景有如下幾種

6.1 訂閱發佈模式,消息隊列

6.2 慢請求,耗時長的任務

6.3 高併發、高性能要求時的多任務處理

6.4 不確定執行的時間點,觸發器

人腦(單核)不擅長非同步思考,電腦(多核)卻更適合。

編程的時候,是人腦適配電腦,還是電腦服務人腦?

在大部分的編程中,大家都只需要考慮同步的方式來寫代碼邏輯。少部分時候,就要考慮使用非同步的方式。而且,有很多的開發框架、類庫已經把非同步處理封裝,可以簡化非同步任務的開發和調試工作。

所以,對於開發者來說,預設還是同步方式思考和開發,當不得不使用非同步的時候,才會考慮非同步的方式。畢竟讓人腦適配電腦,這個過程還是有些困難的。

 

作者:michaeywang,騰訊 IEG 運營開發工程師

本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/Asynchronous-Programming-Guide-North.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 請點贊關註,你的支持對我意義重大。 🔥 Hi,我是小彭。本文已收錄到 GitHub · AndroidFamily 中。這裡有 Android 進階成長知識體系,有志同道合的朋友,關註公眾號 [彭旭銳] 帶你建立核心競爭力。 前言 LeakCanary 是我們非常熟悉記憶體泄漏檢測工具,它能夠幫助開 ...
  • HMS Core機器學習服務文本翻譯能力提供多種語言和多種應用場景的翻譯服務,比如,在出國旅游的場景中,用戶可以藉助應用的語音翻譯播報功能在打車、酒店入住等場景中無障礙溝通,也可以通過拍照翻譯功能讀懂餐廳菜單、路牌信息等。 中文直譯模型讓文本翻譯能力升級 當前主流的翻譯模式大都以語料資源較為豐富的英 ...
  • 當const定義的常量是基本數據類型的時候不可以被更改 當const定義的常量是引用數據類型的時候,其值可以被更改。 文字有點描述不清楚,或者說用什麼存在記憶體什麼的解釋也有點不好理解。直接上圖吧。 重新定義const定義的數值的話,就會出現:Uncaught TypeError: Assignmen ...
  • 本文是深入淺出 ahooks 源碼系列文章的第十一篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,Thanks。 本文來講下 ahooks 中的 useUrlState。 通過 url query 來管理 state 的 Hook。 useUrlState 的特殊 在之前的架構 ...
  • 內聯模板 點擊打開視頻講解更加詳細 當 inline-template 這個特殊的 attribute 出現在一個子組件上時,這個組件將會使用其裡面的內容作為模板,而不是將其作為被分發的內容。這使得模板的撰寫工作更加靈活。 <my-component inline-template> <div> < ...
  • 組件之間的迴圈引用 點擊打開視頻講解更詳細 假設你需要構建一個文件目錄樹,像訪達或資源管理器那樣的。你可能有一個 <tree-folder> 組件,模板是這樣的: <p> <span>{{ folder.name }}</span> <tree-folder-contents :children=" ...
  • 在面向對象出現之前,已有面向過程的分析方法,為什麼面向對象被提出了呢?究其本質原因,人們發現面向過程並不是按照人正常認識事物的方式去分析軟體,那麼人究竟是怎麼認識事物的呢,Yourdon 在《面向對象的分析》一書中提到,人類認識事物是遵循分類學的原理,分類學主要包含三點:區分對象及其屬性;區分整體對... ...
  • MEMS感測器即微機電系統(Micro-electro Mechanical Systems),是指將精密機械繫統與微電子電路技術結合發展出來的一項工程技術,它的尺寸一般在微米量級。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...