[點晴永久免費OA]異步編程真的讓程序更快了嗎?
引言現(xiàn)在異步編程真的是越來越普遍了,從前端的Promise到后端的Channel、Future、Task,異步編程正變得越來越流行。很多同學也玩得很溜了,滿世界的異步調(diào)用,讓程序的效率和用戶體驗都大大提升。不過,當談到為什么要使用異步編程,以及它背后的工作原理時,大部分同學就啞火了。對于一個有追求的程序員來說,我們不僅要會用,更要理解其中的原理,所謂“知其所以然”。 而且異步編程并不是銀彈,本質(zhì)上它不會讓程序運行的更快,使用它也伴隨著復雜的錯誤處理和調(diào)試難題,比如著名的“回調(diào)地獄”。因此,了解它的工作原理,以及正確地使用它,對于編寫高質(zhì)量的代碼來說特別重要。 本文,我們就來一起探討下同步和異步調(diào)用的本質(zhì)區(qū)別,深入解析異步編程的工作原理,以及介紹如何在實際開發(fā)中靈活運用這兩種調(diào)用方式。 概念要討論問題,首先得明確概念,也就是我們到底在說什么。 同步調(diào)用,簡單來說,就是執(zhí)行多個任務的時候,其中一個任務必須完成后,才能開始下一個任務。在這種模式下,任務按照順序依次執(zhí)行,每個任務的執(zhí)行必須等待前一個任務完成,所以大家也稱之為阻塞調(diào)用。 在編程中,同步調(diào)用的一個典型應用場景是數(shù)據(jù)庫事務。比如,在事務中更新一系列的記錄時,系統(tǒng)會按照順序執(zhí)行這些操作,直到全部完成,期間不會去處理其他任務。這確保了數(shù)據(jù)的一致性和完整性,但也意味著在事務處理期間,其他依賴于這些數(shù)據(jù)的操作必須等待。 異步調(diào)用,顧名思義,是一種任務可以在后臺執(zhí)行,而不阻塞當前線程繼續(xù)執(zhí)行其他任務的調(diào)用方式,這可以使多個任務得以并行處理。 在編程中,異步調(diào)用的一個典型應用場景是網(wǎng)絡請求。比如,前端向服務器請求數(shù)據(jù)時,我們可以不需要讓整個應用停下來等待服務器的響應。通過異步調(diào)用,前端可以在等待服務器響應的同時,繼續(xù)執(zhí)行其他任務,比如響應用戶的輸入,這會提高用戶體驗。 簡單來說,同步調(diào)用就像是在排隊取餐,不能走開,而異步調(diào)用則像是掃碼點餐,可以去做其他事情,等飯好了給你送過來。 異步的優(yōu)勢所在更快這里先拋出一個問題:異步會不會讓程序運行的更快? 我們以經(jīng)典的網(wǎng)絡請求場景為例,當客戶端使用異步的方式發(fā)起一次請求后,程序霸占的當前線程就被底層系統(tǒng)分配去干別的事情去了,然后請求會在網(wǎng)絡上傳遞極短的一些時間,到達服務端后再進行一段時間的處理,最后再通過網(wǎng)絡將處理結果返回給客戶端底層系統(tǒng),底層系統(tǒng)再喚起之前的任務繼續(xù)處理。 在這個過程中,網(wǎng)絡來回傳輸?shù)臅r間、服務端處理的時間都沒有受到異步調(diào)用的任何影響,反而可能會因為異步調(diào)用產(chǎn)生任務切換而增加網(wǎng)絡請求的響應時間。所以單次的異步調(diào)用并沒有讓程序運行的更快。 但是但是,異步調(diào)用還是可能會讓程序整體運行的更快。還是以網(wǎng)絡請求場景為例,假設我們需要在頁面上發(fā)起3個網(wǎng)絡請求,每個網(wǎng)絡請求的響應時間都是基本相同的,同步的情況下我們只能一個一個的干,總的響應時間就是單次網(wǎng)絡請求響應時間的3倍,如果換成異步調(diào)用,理想情況下,這三個網(wǎng)絡請求可以在服務端并行處理,而網(wǎng)絡傳輸?shù)臅r間是極短的,那么總的響應時間可能就是一個比單次網(wǎng)絡請求響應時間略高一點的數(shù)字。所以異步調(diào)用相比同步調(diào)用,很有可能會讓程序整體運行的更快。 談到更快時,我們這里一直比較的就是時間,如果網(wǎng)絡傳輸?shù)臅r間、服務端處理的時間都很短,短到就像本地的一次函數(shù)調(diào)用,那么異步也不會讓程序更快。所以根本的問題是網(wǎng)絡傳輸?shù)臅r間太慢、服務端處理的時間太慢,它們相比CPU的處理速度要慢上很多個數(shù)量級,所以這才讓異步有了可乘之機,而異步就是在這些網(wǎng)絡IO、磁盤IO等慢速設備的通信上發(fā)揮主要作用。 更多我們以一個服務端網(wǎng)絡處理程序為例,當請求到達服務端時,程序會給這個請求分配一個線程,用來運行相關的服務端處理程序,假設這個處理中還要調(diào)用別的API,同步調(diào)用和異步調(diào)用就會出現(xiàn)不同的行為了。 同步調(diào)用時,線程會一直等在這里,等待的時候誰也不能搶走這個線程,直到這次內(nèi)部調(diào)用返回結果,然后繼續(xù)處理,直到全部完成,最后返回給調(diào)用方。 異步調(diào)用時,調(diào)用發(fā)起后,線程就被底層系統(tǒng)分配給別的任務了,比如用來接收新的網(wǎng)絡請求,等這次內(nèi)部調(diào)用的結果返回后,底層系統(tǒng)再為本次任務分配線程資源,然后繼續(xù)處理,直到全部完成,最后返回給調(diào)用方。 我們可以看到,在使用異步調(diào)用的情況下,線程的利用率提高了,而這會節(jié)省大量的服務器資源。比如,在Linux系統(tǒng)中,一個線程會占用8M的內(nèi)存資源,那么同步調(diào)用時,8G的內(nèi)存也就能同時接入大概1000個請求,改為異步調(diào)用后,8G的內(nèi)存能同時接入多少請求呢?這里做一個不是很嚴謹?shù)挠嬎悖僭O1個請求的完整處理時間為100毫秒,請求接入到發(fā)起異步調(diào)用的時間為1毫秒,那么使用異步調(diào)用后,8G內(nèi)存就能在這100毫秒內(nèi)接收100倍的請求,也就是10萬個請求。 這也是Go語言、Node.js等可以輕松駕馭高并發(fā)的核心法門。 更省有一種說法是異步調(diào)用后,CPU就去干別的了,不用等著網(wǎng)絡請求返回,所以節(jié)省了CPU資源。其實現(xiàn)代操作系統(tǒng)一般沒有這么傻,它有一套比較科學的CPU調(diào)度算法,CPU并不會傻傻的等著網(wǎng)絡請求返回,除非我們使用特殊的方法霸占著CPU不放。這種說法可能只在古老的操作系統(tǒng)或者一些特殊的嵌入式系統(tǒng)中存在。 異步節(jié)省內(nèi)存資源是實實在在的,同樣的網(wǎng)絡請求數(shù)量下,需要的線程更少了,占用的內(nèi)存也就更少了。 更好的用戶體驗我們可以以一個現(xiàn)代Web應用的實例來說明。當用戶在一個復雜的Web應用中進行操作時,比如提交一個表單,這個表單的數(shù)據(jù)需要通過網(wǎng)絡發(fā)送到服務器。在這個過程中,我們不希望用戶界面凍結或變得無響應。通過使用異步調(diào)用發(fā)送數(shù)據(jù),用戶界面可以繼續(xù)響應其他用戶操作,比如滾動頁面、點擊其他按鈕等。服務器的響應會在數(shù)據(jù)處理完成后返回,這時應用會相應地更新用戶界面,而用戶可能都沒有注意到這個后臺的數(shù)據(jù)交換過程。 異步的實現(xiàn)原理接下來,我們深入探討一下異步是怎么做到上邊這一切的,特別是事件循環(huán)、回調(diào)函數(shù),以及Promises和Async/Await這些概念。以Node.js為例,可以先看看這張圖,下邊會有詳細介紹。 事件循環(huán)在一家餐廳里,有一個廚師(CPU)和一個服務員(事件循環(huán))。當顧客(任務)下單(發(fā)起異步調(diào)用)后,服務員記錄下訂單,然后繼續(xù)服務其他顧客。廚師在后廚準備好食物后,服務員再將食物遞給對應的顧客。這個過程中,服務員不斷的在顧客和廚師之間循環(huán),確保每個顧客的需求都得到滿足,這就是事件循環(huán)的機制。 在不同的操作系統(tǒng)和語言框架中,事件循環(huán)的具體實現(xiàn)可能有所不同,但核心思想是一致的:使得單線程環(huán)境下,可以高效地處理多個異步任務,而不會造成阻塞。 Node.jsNode.js是一個基于Chrome V8引擎的JavaScript運行環(huán)境,它使用事件驅(qū)動、非阻塞IO模型,非常適合處理大量的并發(fā)連接。Node.js的事件循環(huán)由libuv庫實現(xiàn),這個庫專門為了提高Node.js的異步IO性能而設計。 在Node.js中,事件循環(huán)負責執(zhí)行用戶代碼、收集和處理事件,以及執(zhí)行隊列中的子任務。 .NET在.NET框架中,異步編程模型(Asynchronous Programming Model, APM)和基于任務的異步模式(Task-based Asynchronous Pattern, TAP)都是.NET中處理異步操作的方式。.NET中的事件循環(huán)不像Node.js那樣明顯,因為.NET應用通常運行在多線程環(huán)境下,通過線程池(Thread Pool)來處理異步任務。 在.NET中,異步操作通常通過Task來表示,搭配使用async和await關鍵字讓異步代碼的編寫和閱讀更加直觀。.NET運行時會負責調(diào)度這些Task到線程池中的線程上執(zhí)行,從而實現(xiàn)非阻塞的異步操作。 操作系統(tǒng)語言框架的異步處理都是基于操作系統(tǒng)的底層支持。 在操作系統(tǒng)層面,Linux和Windows提供了不同的機制來實現(xiàn)高效的IO事件處理。
語言框架為了實現(xiàn)異步操作,在不同的操作系統(tǒng)上會選擇相應的異步IO處理方式。 回調(diào)函數(shù)回調(diào)函數(shù)就像是你對服務員說:“當我的漢堡準備好了,請通知我。”服務員(事件循環(huán))記下了這個請求,當廚師(CPU)做好漢堡后,服務員會回來通知你。這個過程就是回調(diào)機制。 然而,如果你的要求變得復雜,比如:“我的漢堡準備好后,請通知我,然后我會要求加薯條,薯條準備好后,請再通知我,我可能還會有其他要求……”這樣的多層次回調(diào)會導致所謂的“回調(diào)地獄”,使得代碼難以閱讀和維護。 function prepareBurger(callback) { console.log("開始準備漢堡..."); setTimeout(() => { console.log("漢堡準備好了!"); callback("漢堡"); }, 2000); // 假設準備漢堡需要2秒鐘 } function prepareFries(callback) { console.log("開始準備薯條..."); setTimeout(() => { console.log("薯條準備好了!"); callback("薯條"); }, 1500); // 假設準備薯條需要1.5秒鐘 } // 請求漢堡,然后請求薯條 prepareBurger(function(burger) { console.log("你的" + burger + "已經(jīng)準備好了。"); // 漢堡準備好后,請求薯條 prepareFries(function(fries) { console.log("你的" + fries + "也準備好了。"); // 如果這里還有更多的異步請求,代碼會繼續(xù)嵌套下去... }); }); Promises和Async/Await為了解決“回調(diào)地獄”的問題,現(xiàn)代編程語言引入了Promises和Async/Await,以Javascript為例: Promises 就像是你給服務員下了一個訂單,并得到了一個“承諾”。服務員說:“我保證會告訴你何時你的漢堡準備好。”這樣,你就不需要在柜臺前等待,而是可以去做其他事情,服務員會在承諾的時間里來通知你。 function prepareBurger() { // 返回一個Promise對象 return new Promise((resolve, reject) => { console.log("開始準備漢堡..."); setTimeout(() => { // 模擬漢堡準備過程 console.log("漢堡準備好了!"); resolve("漢堡"); // 成功完成時調(diào)用resolve }, 2000); // 假設準備漢堡需要2秒鐘 }); } // 調(diào)用prepareBurger,并處理結果 prepareBurger().then(burger => { console.log("你的" + burger + "已經(jīng)準備好了。"); }).catch(error => { console.log("出錯了:" + error); }); Promise的寫法看起來還是有點怪異,Async/Await 則是在Promises的基礎上,讓異步代碼看起來更像同步代碼。使用async/await時,你可以用同步的方式寫異步代碼,這讓代碼更加直觀易懂。比如,你對服務員說:“我會在這里等,你準備好漢堡后直接給我。”盡管實際上漢堡的準備是異步的,但對你來說,就像是同步等待結果一樣。 async function getOrder() { try { // 等待prepareBurger完成,并獲取結果 const burger = await prepareBurger(); console.log("你的" + burger + "已經(jīng)準備好了。"); } catch (error) { // 處理可能發(fā)生的錯誤 console.log("出錯了:" + error); } } // 調(diào)用getOrder getOrder(); async/await 其實還利用了協(xié)程的一些處理方式,協(xié)程不是操作系統(tǒng)提供的,而是由編程語言框架在用戶程序中實現(xiàn)的,在異步編程中,它就是用來在IO操作發(fā)起后,將線程分給其它的任務,在IO操作完成后再給任務分配線程。具體到JavaScript中,是通過Generator生成器實現(xiàn)的,它可以控制函數(shù)的暫停和恢復,async/await只是做了一個包裝,實際執(zhí)行時,運行引擎會轉(zhuǎn)換處理。 在 .NET 平臺中,同樣支持使用 async/await 的方式編寫異步代碼,只不過 Promise 變成了 Task。 總結最后,讓我們總結一下同步調(diào)用和異步調(diào)用的區(qū)別,以及它們對軟件開發(fā)的影響。 首先,同步調(diào)用就像是在餐廳里排隊取餐,你得等服務員把飯端上來后才能干別的事情;而異步調(diào)用則像是掃碼點餐,餐點制作的時候,你可以去做任何其他事情。簡而言之,同步調(diào)用會阻塞當前操作直到任務完成,而異步調(diào)用不會,它允許程序在等待過程中繼續(xù)執(zhí)行其他任務。 對軟件開發(fā)來說,這兩種調(diào)用方式的本質(zhì)區(qū)別影響深遠。同步調(diào)用因為簡單直接,適合那些必須順序執(zhí)行、步步為營的任務,特別是計算密集型的任務,異步了也沒有可以節(jié)省的地方;但是,在處理IO操作等耗時任務時,同步調(diào)用可能會導致程序"卡住",既霸占大量的資源,又影響用戶體驗,此時選擇異步調(diào)用則能更有效的利用計算資源,且顯著提高程序的響應性和性能,尤其是在需要大量IO操作的場景下,比如網(wǎng)絡服務器、大型數(shù)據(jù)庫操作等。 轉(zhuǎn)自博客園,作者螢火架構https://www.cnblogs.com/bossma/p/18065866 該文章在 2024/3/28 16:34:59 編輯過 |
關鍵字查詢
相關文章
正在查詢... |