異步編程須要「意識」

雖然咱們生活在一個異步的世界裏,但對於多數編程初學者來講,異步仍是很陌生。學習一門編程語言,一般都是從同步流程開始的,即順序、分支和循環。而異步流程是什麼呢——開始一個異步調用,而後……就沒有而後了。異步程序跑哪去了?編程

異步程序會以某種異步的形式在運行着,好比多線程、異步IO等,直處處理完成。那若是須要處理結果怎麼辦?給一個程序入口,讓它處理完當前過程以後,把處理結果送到這個入口,而後執行另外一段程序——俗稱回調。回調通常使用 callback 這個名稱,不過有時候我更喜歡使用 next,由於它表明着下一個處理步驟。segmentfault

同步和異步的概念

如今咱們接觸到了一些概念,好比同步和異步,它們是什麼?設計模式

這兩個概念並不來源於編程語言,而是來源於低層指令,甚至更低層的——電路。它們是基於時序的兩個概念,其中,「步」是指步調,因此同步表示相同的步調,而異步表示不一樣的步調。固然這兩個概念提高到程序這個級別的時候,精確的意思與時鐘無關,但所表示的意義仍然未變。多線程

同步

舉個生活中的例子來講明這個問題——排除買票。售票廳開了一個窗口,有一隊人在排隊依次買票。這個隊伍中,前面一我的往前走了一步,後面的人才能往前走一步;前面的人在等待,後面的人就必定在等待。那麼在理想的狀況下,全部人能夠同時向前邁步。OK,你們步伐一致,稱爲同步。異步

這裏把售票窗口看做是處理器,每一個人看做是等待執行的指令,買票這個動做就是在執行指令。它的特色是按步就班,若是一我的買票時間過長(指令執行時間過長),就會形成阻塞。async

異步(多線程)

如今買票的人漸漸多起來,因此售票廳多開了幾個窗口同時售票。每一個單獨的隊伍仍然保持着同步,但不一樣的隊伍之間,步伐再也不一致,稱爲異步。A 隊列售票很順利,隊伍在有序快速的前進,但 B 隊列的某個顧客彷佛在付費時遇到點麻煩,花了很長的時候,形成阻塞,但這對 A 隊列並不產生影響。編程語言

這時候的售票廳能夠看做是在以多線程的方式運行着異步程序。從這個例子能夠看到異步的兩個特色:其一,兩個異步流程之間相互獨立,它們相互不會阻塞(有個前提,不須要等待共享資源的狀況下);其二,異步程序內部仍然是同步的ide

異步(IO)

上面的例子比較符合多線程異步的狀況。那 IO 異步又是什麼樣呢?模塊化

年末了,M 在準備年終彙報的資料,這但是個緊張的工做(CPU),要收集很多數據來寫好些文案。爲了其中一份文案,M 須要車間的生產數據,但跑一趟車間(IO)可須要花很多時間,因此他讓 N 去車間收集數據,本身則繼續寫其它方案,同時等 N 把數據收集回來(啓動異步程序)。半天之後,N 帶回了數據(插入事件消息),M 繼續完成手上的文案(完成當前事件循環),以後使用 N 帶回來的數據開始撰寫關於車間的報告(新的事件循環)……異步編程

IO 的處理速度比 CPU 慢得多,因此 IO 異步讓 CPU 沒必要閒置着等待 IO 操做完成。當 IO 操做完成以後,CPU 會適地使用 IO 操做結果繼續工做。

同步邏輯和異步邏輯

回到程序上來,咱們以一個函數的處理過程來描述同步和異步的處理方式。

同步邏輯

那麼,同步處理過程是:

接受輸入 ⇒ 處理 ⇒ 產生輸出

用一段僞代碼來描述就是

注:本文中的僞代碼比較接近 JavaScript 語法,而有時候爲了說明類型,採用了 TypeScript 的類型申明語法。

function func(input) {
    do something with input
    return output
}

這是標準的 IPO(Input-Process-Output) 處理。

異步邏輯

而異步呢,是:

接受輸入 ⇒ 處理 ⇒ 啓動下一步(若是有)

用僞代碼來描述就是:

function asyncFunc(input, next) {
    do something with input
    if (next is a entry) {
        next(output)
    }
}

這個過程稱爲 IPN(Input-Process-Next)。

注意到這裏的 Next,下一步,只有一步。這一步,囊括了後續的若干步驟。因此這一步,只能是後續若干步驟封裝出來一個模塊入口,或者說函數。

所以,模塊化思想在異步思惟中是一個很是關鍵的思想。不少初學者寫代碼喜歡像記流水帳同樣一句句往下寫,動不動就是成百上千行的函數,這就是一種缺少模塊化思想的表現。模塊化思想須要訓練,分析代碼的相關性,提煉函數,提取對象,在具備必定經驗以後還須要掌握模塊細化的粒度平衡。這不是一朝一夕之功,不過我推薦看看「設計模式」和「重構」相關的書籍。

異步開發工具(SDK和語法層面的)

承諾(Promise)

再想一想上面關於年終彙報的例子,M 請 N 去車間收集數據的時候,N 會說:「好的,我很快就把數據帶回來」,這是一種承諾。基於這個承諾,M 才能安排後面撰寫關於車間的彙報材料。這個過程用僞代碼來描述就是

function collectData(): Promise {
    // N 去收集數據,產生了一個承諾
    return new Promise(resolve => {
        collect data from workshop
        // 這個承諾最終會帶來數據
        resolve(data)
    })
}

function writeWorkshopReport(data) {
    write report with data
}

// 收集數據的承諾兌現以後,可將這個數據用於寫報告
collectData()
    .then(data => writeWorkshopReport(data))

以 JavaScript 爲表明的一些語言 SDK 中使用了 Promise。不過 C# 中是採用的 TaskTask<T>,相應的,使用了 Task.ContinueWithTask<T>.ContinueWith 來代替 Promise.then

異步邏輯同步化

上面提到了同步思惟和異步思惟在一個處理步驟中的區別。若是跳出一個處理步驟,從更大範圍的處理流程來看,異步與同步其實也沒多大區別,都是 輸入-->處理-->產生輸出-->將輸出用於下一步驟,惟一要注意的是須要等待異步處理產生的輸出,咱們能夠稱之爲異步等待。因爲咱們能夠一邊進行異步等待(async wait,簡寫 await),一邊作別的事情,因此這個等待並不產生阻塞。可是,因爲聲明瞭這個等待,編譯器/解釋器會將後面的代碼自動放在等待完成以後調用,這讓異步代碼寫起來就像寫同步代碼同樣。

上面的例子使用異步等待的僞代碼會像這樣

async function collectData(): Promise {
    collect data from workshop
    // 多數語言會把 async 函數的返回值封裝成 Promise
    return data
}

function writeWorkshopReport(data) {
    write report with data
}

// await 只能用於聲明爲 async 的函數中
async function main() {
    data = await collectData()
    writeWorkshopReport(data)
}

// 定義了異步 main 函數,必定要記得調用,否則它是不會執行的
main()

像 C# 和 JavaScript 等語言都從語法層面規定了 await 必須用在聲明爲 async 的函數中,這就從編譯/解釋的層面限定了 await 的用途,只要使用了 await,那它所處的就必定是一個異步上下文。而 async 也要求編譯器/解釋器對其返回值進行一些自動處理,好比在 JavaScript 中,其返回值若是不是 Promise 對象,它會自動封裝成一個 Promise 對象;而在 C# 中,它會自動封裝成 TaskTask<T>(因此 async 方法的類型須要聲明爲 TaskTask<T>)。

注意,注意,注意

儘管語言服務在異步程序同步化方面已經作了不少工做,可是仍然避免不了一些人爲錯誤,好比忘記寫 await 關鍵字。在強類型語言中編譯器會檢查得嚴格一些,但若是是在 JavaScript 中,忘記寫 await 意味着本來應該取得一個值的語句,會取到一個 Promise。解釋器不會對此質疑,但程序運行的結果會不正確。

小結

總的來講,異步編程並非特別困難的事情。使用 async/await 語言特性甚至能夠用相似編寫同步代碼的方法來編寫異步代碼。但語法糖終究是糖,要想把異步編程掌握得更好,仍是須要去了解和熟悉異步、回調、Promise、模塊化、設計模式、重構等概念。

相關閱讀

相關文章
相關標籤/搜索