Guide 開髮指南

同步和非同步

引言

隨著Web 應用的不斷發展,JavaScript 作為一種廣泛使用的程式語言,也不斷地發展和演變。在 Web 前端開發中,JavaScript 主要用於瀏覽器的UI 處理,UI 開發是典型的單執行緒事件驅動模式,因此JavaScript 也形成了以非同步處理為主要程式設計範式。但是在大規模複雜的應用中,非同步程式設計帶來的問題和複雜性也越來越明顯。

Node.js 的出現為JavaScript 帶來了一個全新的非同步程式設計範式:事件循環和回呼函數。這種程式設計範式具有高效、簡潔的特點,適用於高並發、I/O 密集的場景。然而,這種程式設計範式也帶來了它自己的問題和複雜性,尤其是在大規模複雜的應用中,程式設計師需要處理很多回呼函數巢狀的問題,並且需要處理非同步呼叫順序的問題,增加了程序的複雜性和難度。

為了解決這些問題和困難點,fibjs 應運而生。 fibjs 是一個主要為web 後端開發而設計的應用程式伺服器開發框架,它建立在Google v8 JavaScript 引擎基礎上,並且選擇了和傳統的callback 不同的並發解決方案。 fibjs 利用fiber 在框架層隔離了非同步呼叫所帶來的業務複雜性,極大降低了開發難度,並減少因為使用者空間頻繁非同步處理所帶來的效能問題。同時,與傳統的非同步程式設計範式相比,它的同步程式設計範式更可讀性強、邏輯簡單、易於維護的優勢。

在接下來的內容中,我們將介紹fibjs 的優點和特點,並透過實例來講解如何使用它的同步和非同步程式解決方案。

fiber 簡介

fibjs 是一個基於v8 引擎的高效能的JavaScript 伺服器框架,主要在Web 後端開發。始於2009 年,目前已經擁有了很高的穩定性和生產力,在國內外有著廣泛的應用案例。

在fibjs 中,fiber 被用來解決業務邏輯和I/O 處理之間的問題。 fiber 與傳統的線程、協程、進程等概念不同,它是一種用戶級的輕量級線程,可以看作是一種協作式多任務處理機制。 fiber 可以在不同的上下文中執行業務邏輯和I/O 操作,內部透過預先分配和循環利用來管理資源,相比於傳統的線程和進程,它具有更輕量級、更靈活、更高效的特點。

與其它線程庫(如pthread、WinThread、Boost.Thread 等)相比,fiber 有以下優勢:

  • 協作式調度:fiber 是協作式調度,不需要核心或作業系統搶佔式調度,減少了頻繁地上下文切換,加快了程式的運行速度,同時避免了執行緒之間的競爭條件和死鎖問題。

  • 輕量級:每個fiber 只需消耗一個較小的堆疊空間,在多重並發應用中可以創建大量的fiber,不會導致佔用過多記憶體的問題。

  • 高效性:fiber 是基於JavaScript 語言本身的特性實現,並且充分利用了v8 引擎的優越性能,速度比傳統的線程庫更快。

透過使用fiber,fibjs 可以將業務邏輯和I/O 處理分離,從而將非同步呼叫封裝成同步呼叫的形式,使得編寫和維護程式碼更加簡單且易讀,同時可以充分發揮JavaScript 語言的優點。

fibjs 中的同步編程

在非同步程式設計中,由於回呼函數的巢狀有可能導致程式碼的可讀性變差,容易產生回呼地獄的問題,增加程式碼的難度和除錯的成本。而同步程式設計範式更符合人類的思考模式,使得程式碼結構更加清晰、易讀、易於維護,可以大幅提升開發效率和程式碼品質。

在fibjs 中,同步程式設計是一種十分流行且常用的程式設計範式,它使得程式碼的結構和邏輯更加直觀,易於理解和維護。一些同步編程的函數和模組在fibjs 中得到了高度支持,例如util.sync、fs.readSync 等。

在fibjs 中,可以直接以同步方式呼叫內建物件的非同步函數:

1 2 3 4
const fs = require("fs"); const data = fs.readFile("/path/to/file"); console.log(data);

也可以把非同步函數透過util.sync 和try…catch 包裝一下,可以讓fiber 得到非同步呼叫的回傳值,從而實現同步的效果,例如:

1 2 3 4 5 6 7 8 9 10 11 12 13
// load module const coroutine = require("coroutine"); const util = require("util"); const fs = require("fs"); // use util.sync to wrap fs.readFile function readFile(path) { return util.sync(fs.readFile)(path); } // call the sync function const data = readFile("myfile.txt"); console.log(data);

在上面的範例中,我們定義了一個名為readFile 的函數,利用util.sync 將非同步的fs.readFile 函數封裝成了同步函數,這個函數可以透過同步呼叫的方式直接傳回資料。這種同步呼叫方式和傳統的JavaScript 程式設計範式類似,不同的是,在fibjs 中不會阻塞線程,而是透過fiber 實現非同步效果。

util.sync 的原理

util.sync 是核心的一個高效的包裹函數,而下面的JavaScript 程式碼可以實現類似的功能:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
const coroutine = require("coroutine"); function sync(func) { return function _warp() { var ev = new coroutine.Event(); var e, r; func.apply(this, [ ...arguments, function (err, result) { e = err; r = result; ev.set(); } ]); ev.wait(); if (e) throw e; return r; } }

這段程式碼定義了一個用於將非同步回呼函數轉換為同步呼叫函數的工具函數sync。它接收一個函數func,並傳回一個新的函數_wrap。這個新函數實作了將原函數轉換為同步呼叫的功能。在_wrap 函數中,首先建立了一個新的Event 物件ev,用於執行緒調度和等待非同步回調結果。之後使用apply 方法將指定參數和新的回呼函數作為參數,呼叫原函數func。在呼叫的過程中,發生了非同步回調,新的回呼函數將傳回的結果儲存到變數e 和r 中,並喚醒Event 物件。最後根據變數e 來決定是否拋出異常,或傳回變數r。這個函數實現了將非同步回呼函數轉換為同步呼叫的解決方案,能夠提高函數的可讀性和可維護性。

fibjs 中的非同步編程

在fibjs 中,大多數非同步方法(包括I/O 和網路請求方法等)都可以同時支援同步和非同步調用,這使得開發者可以隨時根據自己的程式需求來選擇使用哪種方式。

以fs.readFile() 為例,我們可以用兩種方式來使用此方法:

非同步方式:透過傳遞一個回呼函數來處理讀取檔案的結果,例如:

1 2 3 4 5 6
const fs = require("fs"); fs.readFile("/path/to/file", (err, data) => { if (err) throw err; console.log(data); });

這種方式適用於需要在讀取檔案完成後執行某些操作的情況。

同步方式:透過不傳遞回呼函數來獲得文件的內容,例如:

1 2 3 4
const fs = require("fs"); const data = fs.readFile("/path/to/file"); console.log(data);

在此範例中,我們透過讀取檔案的回傳值data 來取得檔案的內容,不需要在等待檔案讀取完成的回呼函數執行完畢後才能繼續執行操作。這種方式適用於需要在讀取檔案完成前執行某些操作的情況。

可以看到,這種同時支援同步和非同步呼叫的特點,使得開發者可以根據自己的需求和開發場景選擇使用不同的方式。在某些情況下,同步方式的程式碼可讀性更高,更易於維護和調試;而在某些情況下,非同步方式可以更好地提高程式碼的回應速度和效能。

然而,在使用同步方式的時候也需要注意,在某些場景下,這種方式可能會阻塞目前fiber。因此,我們需要根據實際需求來選擇合適的程式設計方式。

結論

在本文中,我們介紹了fibjs 的同步程式設計風格和非同步程式解決方案以及它們的優點和應用場景。我們提到,fibjs 能夠透過利用fiber 隔離業務邏輯和非同步處理所帶來的效能問題,降低操作複雜度並提高程式碼開發效率。同時,我們也強調了fibjs 在I/O 處理和記憶體管理等方面的優勢,這為開發、測試和維護帶來了極大的便利。

最後,我們鼓勵讀者深入探索fibjs,以及參與fibjs 的貢獻和社區活動。我們相信,fibjs 會繼續以它強大的效能和易用性不斷吸引開源社群的關注和支援。

👉 【服務端模組熱更新