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