<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Node.js 異步延續(xù)模型

          共 9460字,需瀏覽 19分鐘

           ·

          2020-10-06 08:08

          作者:昭朗

          來源:Node地下鐵

          異步執(zhí)行在 Node.js 中是非常基本的操作,但是一個(gè) Uncaught Exception 的報(bào)錯(cuò)就可能讓我們摸不著頭腦,是什么地址的 TLS 訪問 ECONNRESET 了?

          [node:12345] Uncaught Exception: Error: read ECONNRESET
          ? ?at TLSWrap.onStreamRead (internal/stream_base_commons.js:111:27)
          ? ?// 真的沒了

          異步延續(xù)模型

          Node.js 使用的 JavaScript 單線程執(zhí)行模型簡化了非常多的問題。而為了防止 IO 阻塞 JavaScript 執(zhí)行線程,IO 操作在關(guān)聯(lián)了 JavaScript 回調(diào)函數(shù)后就被放入了后臺處理。當(dāng) IO 操作完成后,與其關(guān)聯(lián)的 JavaScript 回調(diào)會被放入事件隊(duì)列等待在 JavaScript 線程調(diào)用,可以在這里[鏈接1]了解更多 Node.js 事件循環(huán)的詳情。

          這個(gè)模型有很多好處,但是也有一個(gè)關(guān)鍵的挑戰(zhàn):異步資源與操作的上下文管理。什么是異步操作上下文?異步操作的上下文就是給定一個(gè)異步操作,我們能夠通過異步上下文知道這個(gè)異步操作是因?yàn)槭裁从|發(fā)執(zhí)行的,接下來可以觸發(fā)其他什么異步操作。Semantics?of Asynchronous?JavaScript [鏈接2] 通過非常精確的描述方法描述了異步資源的“上下文”,但是我們只想回答一個(gè)問題,在程序的任意一個(gè)執(zhí)行時(shí)間點(diǎn),“我們是通過什么樣的異步函數(shù)執(zhí)行路徑執(zhí)行到現(xiàn)在這個(gè)代碼位置的”?

          為了回答這個(gè)問題,我們先明確幾個(gè)關(guān)鍵點(diǎn):

          • 執(zhí)行楨 (Execution Frame) - 執(zhí)行楨是程序中后繼函數(shù)的一次執(zhí)行過程。可以把執(zhí)行楨當(dāng)作從一個(gè)特定的函數(shù)執(zhí)行楨被推入執(zhí)行棧開始,直到這個(gè)執(zhí)行楨執(zhí)行結(jié)束被彈出調(diào)用棧,這樣一段時(shí)間片段。不是所有的函數(shù)都是后繼函數(shù) (Continuation),一個(gè)特定的后繼函數(shù)可以被調(diào)用多次,每一次執(zhí)行都對應(yīng)一個(gè)獨(dú)立的執(zhí)行楨。

          • 后繼函數(shù) (Continuation) - 后繼函數(shù)是在執(zhí)行楨中創(chuàng)建的 JavaScript 函數(shù),并且會在后續(xù)被作為異步回調(diào)執(zhí)行。當(dāng)被調(diào)用時(shí),后繼函數(shù)會創(chuàng)建一個(gè)獨(dú)立的執(zhí)行楨。比如當(dāng)我們調(diào)用 setTimeout(function c() {}, 1000)setTimeout 是在一個(gè)執(zhí)行楨里被調(diào)用的,并且以一個(gè)后繼函數(shù) c 作為參數(shù)。當(dāng) c 在計(jì)時(shí)到時(shí)后被執(zhí)行時(shí),會創(chuàng)建一個(gè)新的執(zhí)行楨;當(dāng) c 執(zhí)行結(jié)束時(shí),即意味著這個(gè)新創(chuàng)建的執(zhí)行楨執(zhí)行結(jié)束。

          • 后繼點(diǎn) (Continuration Point) - 后繼點(diǎn)是接受一個(gè)后繼函數(shù)作為參數(shù)的函數(shù)。通常在 JavaScript 中后繼點(diǎn)都是宿主環(huán)境定義的(ECMAScript 規(guī)范中中不存在與 IO 相關(guān)的操作的定義)。比如 setTimeout,也包括 Promise.then。值得注意的是,不是所有接收函數(shù)作為參數(shù)的函數(shù)都是后繼點(diǎn),作為后繼點(diǎn),這些函數(shù)參數(shù)需要被異步調(diào)用,即不在當(dāng)前執(zhí)行楨被調(diào)用執(zhí)行。比如 Array.prototype.forEach 就不算后繼點(diǎn)。

          • 鏈接點(diǎn) (Link Point) - 程序運(yùn)行中,當(dāng)一個(gè)后繼點(diǎn)被調(diào)用時(shí),我們稱為鏈接點(diǎn)。這個(gè)時(shí)候需要在當(dāng)前執(zhí)行楨與被作為參數(shù)傳入的后繼函數(shù)之間創(chuàng)建一個(gè)邏輯連接,作為上下文綁定。

          • 就緒點(diǎn) (Ready Point) - 就緒點(diǎn)是程序運(yùn)行中之前鏈接過的后繼函數(shù)被標(biāo)記為“準(zhǔn)備就緒”,準(zhǔn)備被執(zhí)行。這個(gè)過程會在后繼函數(shù)與當(dāng)前執(zhí)行楨之間建立邏輯連接,這個(gè)邏輯連接可以稱為因果關(guān)系。通常就緒點(diǎn)都需要在鏈接點(diǎn)后才能產(chǎn)生,但是 Promise 在這里的處理不太一樣,promise 可以在他的 Promise 鏈中的前置的 promise 被 resolve 時(shí)產(chǎn)生一個(gè)就緒點(diǎn),而此時(shí)不一定需要已經(jīng)生成鏈接點(diǎn)(綁定回調(diào)函數(shù)),如 new Promise(res => res()) 立刻創(chuàng)建了一個(gè)已經(jīng) resolve 的 Promise,此時(shí)已經(jīng)觸發(fā)了就緒點(diǎn),但是我們還未通過 .then 鏈接上下一個(gè) Promise。

          而上述幾個(gè)關(guān)鍵點(diǎn)可以總結(jié)為以下幾個(gè)事件:

          • 執(zhí)行開始 (executionBegin): 表示一個(gè)執(zhí)行楨開始執(zhí)行;

          • 鏈接 (link): 表示一個(gè)后繼點(diǎn)被調(diào)用,一個(gè)后繼函數(shù)被放入等待隊(duì)列等待就緒;

          • 就緒 (ready): 表示一個(gè)就緒點(diǎn)被觸發(fā);

          • 執(zhí)行結(jié)束 (executionEnd): 表示一個(gè)執(zhí)行楨執(zhí)行完畢。

          這里我們以下面這段代碼舉個(gè)例子:

          console.log('starting');
          Promise p = new Promise((reject, resolve) => {
          ?setTimeout(function f1() {
          ? ?console.log('resolving promise');
          ? ?resolve();
          }, 100);
          });
          p.then(function f2() {
          ?console.log('in then');
          }

          這段代碼可以通過以下的事件流描述整個(gè)異步執(zhí)行過程:

          { "event": "executionBegin", "executeID": 0 } // 程序開始執(zhí)行
          // starting
          { "event": "link", "executeID": 0, "linkID": 1} // `f1()` 已經(jīng)被鏈接到了 "setTimeout()" 的調(diào)用上
          { "event": "link", "executeID": 0, "linkID": 2} // `f2()` 已經(jīng)被鏈接到了 "p.then()" 的調(diào)用上
          { "event": "executionEnd", "executeID": 0 } // 程序外層代碼執(zhí)行完畢

          { "event": "ready", "executeID": 0, "linkID": 1, "readyID": 3 } // 100ms 計(jì)時(shí)到時(shí),執(zhí)行就緒
          { "event": "executionBegin", "executeID": 4, "readyID": 3 } // f1() 回調(diào)開始執(zhí)行
          // resolving promise
          { "event": "ready", "executeID": 4, "linkID": 2, "readyID": 5 } // promise p 被 resolve,標(biāo)記了 "then(function f2()..." 就緒
          { "event": "executionEnd", "executeID": 4 } // f1() 回調(diào)執(zhí)行完畢

          { "event": "executionBegin", "executeID": 6, "readyID": 5 } // f2() 回調(diào)開始執(zhí)行
          // in then
          { "event": "executionEnd", "executeID": 6 } // f2() 回調(diào)執(zhí)行完畢

          現(xiàn)有技術(shù)

          async_hooks

          async_hooks 即是 Node.js 對上述模型的實(shí)現(xiàn)。其中 async_hooks API 提供了幾個(gè)異步階段的鉤子回調(diào)可以注冊:

          • [init(asyncId, type, triggerAsyncId, resource): void](https://nodejs.org/api/async_hooks.html#async_hooks_init_asyncid_type_triggerasyncid_resource) - 表示 asyncId 對應(yīng)的異步資源(可以理解為上文中的異步上下文)初始化完成,后續(xù)這個(gè)資源可以觸發(fā)異步回調(diào)(不絕對會觸發(fā),比如一個(gè) HTTP Server 啟動后沒人來請求);

          • [before(asyncId): void](https://nodejs.org/api/async_hooks.html#async_hooks_before_asyncid) - 表示準(zhǔn)備開始執(zhí)行異步回調(diào)函數(shù),而在這個(gè)異步回調(diào)函數(shù)的執(zhí)行楨中,生成的任意異步資源(相當(dāng)于上文中的后繼函數(shù))都會與 asyncId 參數(shù)對應(yīng)的異步資源鏈接,作為觸發(fā)原因;

          • [after(asyncId): void](https://nodejs.org/api/async_hooks.html#async_hooks_after_asyncid) - 表示異步回調(diào)函數(shù)執(zhí)行完畢,停止將asyncId 參數(shù)關(guān)聯(lián)的異步資源與當(dāng)前執(zhí)行楨新創(chuàng)建的異步資源鏈接;

          • [destroy(asyncId): void](https://nodejs.org/api/async_hooks.html#async_hooks_destroy_asyncid) - 表示 asyncId 參數(shù)對應(yīng)的 異步資源被回收,后續(xù)不可能再通過這個(gè)異步資源觸發(fā)回調(diào)。

          domain 的區(qū)別

          部分了解、使用過 domain 模塊的同學(xué)可能會有一個(gè)疑問,async_hooks API 與被廢棄的 domain 有什么區(qū)別?

          async_hooks 作為上述異步模型中將各個(gè)異步資源鏈接起來的黏合劑,其本身并不提供任何錯(cuò)誤處理相關(guān)的 API,他的 API 語義也非常清晰,只是對于異步資源的執(zhí)行事件的描述。而 domain 的主要用途是異步錯(cuò)誤的處理,但是因?yàn)樵?domain 提出的時(shí)候還不存在 async_hooks,并且對于異步資源、異步執(zhí)行的語義定義并不清晰,從而導(dǎo)致實(shí)際生產(chǎn)中 domain 的使用非常容易導(dǎo)致錯(cuò)誤并且難以排查(多個(gè) domain 的使用方其中如果使用了不是那么正確的方法,會將 domain 的狀態(tài)攪得一團(tuán)糟)。

          而在 async_hooks 實(shí)現(xiàn)了明確的異步資源與執(zhí)行的語義后,domain 的實(shí)現(xiàn)也進(jìn)行了遷移、使用 async_hooks 來實(shí)現(xiàn)對于異步資源回調(diào)的追蹤(實(shí)現(xiàn)詳情可以了解 PR[鏈接3])。

          Node.js Add-on 的兼容性

          雖然 Node.js 提供的 IO 操作的異步回調(diào)都已經(jīng)被妥善地封裝了異步調(diào)用的上下文切換,但是 Node.js 還提供了 C/C++ Add-on 的 API,這些 Add-on 普通的 napi_call_function 調(diào)用并不會被當(dāng)成是一個(gè)新的執(zhí)行楨,就如同一個(gè) JavaScript 函數(shù)中調(diào)用另一個(gè) JavaScript 函數(shù)。但是如果 Add-on 在異步回調(diào)中也簡單地使用 napi_call_function 就有可能導(dǎo)致 async_hooks 所提供的異步資源 API 出現(xiàn)漏洞。所以 Add-on 需要按照 async_hooks 提供的鉤子的語義,在各個(gè)關(guān)鍵時(shí)間點(diǎn)通過異步資源 API 注冊上,即可完善整個(gè)異步調(diào)用鏈路。但是這樣會給 Add-on 開發(fā)過程造成了一定的負(fù)擔(dān),而為了降低 Add-on 開發(fā)過程出現(xiàn)紕漏的可能。N-API 提供了線程安全的回調(diào) JavaScript 線程的 napi_threadsafe_function 機(jī)制,并且已經(jīng)與異步資源綁定,不需要我們再關(guān)心異步資源的事件管理。

          #include 
          #include

          void async_call_js(napi_env env,
          ? ? ? ? ? ? ? ? ? napi_value js_callback,
          ? ? ? ? ? ? ? ? ? void* context,
          ? ? ? ? ? ? ? ? ? void* data)
          {
          ?napi_status status;
          ?// 將 data 轉(zhuǎn)換成 JavaScript 值
          ?napi_value value = transform(env, data);
          ?napi_value recv;
          ?status = napi_get_null(env, &recv);
          ?assert(status == napi_ok);
          ?// N-API 已經(jīng)為我們綁定了異步資源,這里可以安全地使用 `napi_call_function`
          ?napi_value ret;
          ?status = napi_call_function(env, recv, js_callback, 1, &value, &ret);
          ?assert(status == napi_ok);
          }

          // 會在工作線程被調(diào)用的工作函數(shù)
          void do_work(napi_threadsafe_function tsfn)
          {
          ?/** work, work. */
          ?napi_status status = napi_call_threadsafe_function(tsfn, data, napi_tsfn_nonblocking);
          ?assert(status == napi_ok);
          }

          napi_value some_module_method(napi_env env, napi_callback_info info)
          {
          ?napi_status status;
          ?// 創(chuàng)建與 AsyncResource 綁定的 ThreadSafe Function
          ?napi_threadsafe_function tsfn;
          ?status = napi_create_threadsafe_function(env,
          ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? func,
          ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? async_resource,
          ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? async_resource_name,
          ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? max_queue_size,
          ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? initial_thread_count,
          ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? finalize_data,
          ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? finalize_cb,
          ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? context,
          ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? call_js_cb,
          ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? &tsfn);
          ?assert(status == napi_ok);

          ?// 創(chuàng)建工作線程
          ?create_worker(tsfn, /** 其他參數(shù) */);

          ?// 返回 JavaScript 值..
          ?napi_value ret;
          ?status = napi_get_null(env, &ret);
          ?assert(status == napi_ok);
          ?return ret;
          }

          使用場景

          異步任務(wù)調(diào)度

          在單元測試中,如果我們使用了異步任務(wù),一個(gè)可能比較常見的場景就是這個(gè)異步任務(wù)可能會泄漏出我們的測試函數(shù)執(zhí)行楨導(dǎo)致我們后續(xù)無法追蹤、或者影響了后續(xù)的測試結(jié)果。

          我們來看一個(gè)例子:

          it('should wait for async test', () => {
          ?setTimeout(() => {
          ? ?Promise.resolve(0).then(() => {
          ? ? ?// only after this code executes will the test complete.
          ? ? ?console.log('wait for me'));
          ? }
          }, 0);
          });

          在這個(gè)例子中,我們可以看到其中 setTimeout 逃逸出了測試執(zhí)行楨,從而導(dǎo)致測試提早結(jié)束,并且可能影響后續(xù)測試任務(wù)的運(yùn)行(比如在 setTimeout 中拋出了異常)。現(xiàn)在我們可以通過將全部的方法都使用 callback、promise 給串起來,但是這畢竟需要開發(fā)者自行去完成,并且可能出現(xiàn)疏漏,還是會出現(xiàn)例子中的情況。那么我們有沒有可能從語言運(yùn)行時(shí)層面提供一個(gè)“完美”的方案來跟蹤所有的異步任務(wù)呢?通過 async_hooks 的異步資源追蹤能力,我們就可以標(biāo)記所有在測試執(zhí)行過程中創(chuàng)建的異步資源,如果在測試執(zhí)行結(jié)束后,還存在未銷毀的異步資源,就可以更早地將問題暴露。

          如我們有下面這個(gè)例子:

          const assert = require('assert');
          const {createHook, AsyncLocalStorage} = require('async_hooks');

          const als = new AsyncLocalStorage();
          const backlog = new Map();
          createHook({
          ?init (asyncId, type, triggerAsyncId, resource) {
          ? ?const test = als.getStore();
          ? ?if (test == null) {
          ? ? ?return;
          ? }
          ? ?backlog.set(asyncId, { type, triggerAsyncId, resource });
          },
          ?destroy (asyncId) {
          ? ?backlog.delete(asyncId);
          },
          ?promiseResolve (asyncId) {
          ? ?backlog.delete(asyncId);
          }
          }).enable();

          const queue = []
          function test(name, callback) {
          ?queue.push({ name, callback });
          }

          function run() {
          ?if (queue.length === 0) {
          ? ?return;
          }
          ?const { name, callback } = queue.pop();
          ?als.run(name, async () => {
          ? ?try {
          ? ? ?await callback();
          ? } finally {
          ? ? ?als.exit(() => {
          ? ? ? ?setImmediate(() => {
          ? ? ? ? ?assert(backlog.size === 0, `'${name}' ended with dangling async tasks.`);
          ? ? ? ? ?run();
          ? ? ? });
          ? ? });
          ? }
          });
          }
          process.nextTick(run);

          /** 測試聲明開始 */
          test('foo', async () => {
          ?await new Promise(res => setTimeout(res, 100));
          ?// Pass.
          });
          test('bar', async () => {
          ?setTimeout(res, 100);
          ?// Assert Failed => 'bar' ended with dangling async tasks.
          });

          在這個(gè)例子中,每一次測試開始執(zhí)行前,我們都會在為測試運(yùn)行注冊一個(gè)異步特有數(shù)據(jù)存儲,然后再開始執(zhí)行測試,這樣在測試中發(fā)起的所有異步資源都會被 async_hooks 捕捉到并被測試模塊標(biāo)記,直到這個(gè)異步資源被銷毀(或者是 Promise Resolve)。隨后在測試結(jié)束后,我們再檢查當(dāng)前測試是否有遺留的異步資源,即可確認(rèn)我們的測試是干凈無殘留的。

          異步調(diào)用棧/性能診斷

          這也是我們開頭的問題。

          在越來越多大型的項(xiàng)目使用 Node.js 作為研發(fā)技術(shù)棧后,開發(fā)者們也會越來越關(guān)注問題的診斷便捷性。除了異常錯(cuò)誤排查,現(xiàn)在我們也可以通過 Chrome DevTools 的 CPU Profiler 亦或者是生成火焰圖來診斷我們的 Node.js 應(yīng)用性能表現(xiàn),但是這些工具現(xiàn)在更多的是只能查看某一個(gè)函數(shù)在單個(gè)同步執(zhí)行楨中的調(diào)用鏈路與時(shí)間占用比例,并沒有能力將一個(gè)異步鏈路上每一個(gè)異步操作所花費(fèi)的時(shí)間與百分比描繪出來。

          而在能夠串聯(lián)異步鏈路中的異步調(diào)用棧之后,后續(xù)我們也可以在開發(fā)中使用更加直觀的性能剖析工具:

          或者是提供線上的請求鏈路追蹤能力,就如同現(xiàn)在各種成熟 APM 提供的應(yīng)用間 RPC 調(diào)用鏈路一樣,我們同樣也可以繪制出應(yīng)用內(nèi)一個(gè)請求到底經(jīng)歷了什么流程,每一步分別花費(fèi)了多少時(shí)間:

          AsyncLocalStorage

          使用線程作為處理單元的模型中,我們可以使用 ThreadLocal 來存儲對于當(dāng)前線程特有的數(shù)據(jù)信息,那么在 Node.js 的異步模型中,我們有什么辦法可以方便地存儲對于當(dāng)前異步任務(wù)來說特有的數(shù)據(jù)信息呢?

          Node.js 在 3 月 4 日發(fā)行的 v13.10.0 版本第一次發(fā)布了 async_hooks.AsyncLocalStorage,可以在異步回調(diào)或者 Promise 中獲取異步調(diào)用的狀態(tài)信息,比如 HTTP 服務(wù)器在處理請求的異步鏈路中的任意一步都可以訪問對于這個(gè)請求而言專有的數(shù)據(jù)。

          后續(xù)

          除了在 Node.js 中我們需要清晰的異步執(zhí)行模型的定義之外,同樣提供了 JavaScript 執(zhí)行環(huán)境的瀏覽器中在 JavaScript 項(xiàng)目日漸復(fù)雜之后同樣也需要更能描寫異步時(shí)間線的診斷能力。除此之外,其實(shí) Node.js 的 async_hooks 接口本身并不容易被更多的用戶所使用,他暴露了異步資源非常底層的屬性,雖然這些接口能夠準(zhǔn)確描述我們的異步資源,但是想要利用好這些接口并不簡單。

          鏈接

          Node.js 事件循環(huán):https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

          Semantics of Asynchronous JavaScript:https://www.microsoft.com/en-us/research/wp-content/uploads/2017/08/NodeAsyncContext.pdf

          domain: re-implement domain over async_hook:https://github.com/nodejs/node/pull/16222



          》》面試官都在用的題庫,快來看看《

          瀏覽 54
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天天干天天碰 | 国产一级18 片视频 | 青草久久視頻網站 | 免费观看黄色的网站 | 中文人妻无码一区二区三区久久 |