這篇我們要來看 Effect 是怎麼調度我們的程式的執行流程的,如果你之前有碰過一些比較偏向系統程式的東西,你可能會知道, fiber 指的是協程,意即一種需要執行中的任務主動放棄處理器的控制權,來達成的排程機制
而 Effect 也實作了 fiber 的這個概念,在 js 的程式中將任務進行排程。到這邊你可能會想,我們寫程式一直都在執行,什麼時候主動放棄過控制權的?
答案是當你在 Effect.gen 使用 yield 時或是在 pipe 中,從一個 Effect 切換到下一個 Effect 的中間,這些時間點都會導致控制權回到 Effect 的調度器,讓調度器有機會安排如何執行 Effect
我們接下來可以來看一個例子,不過在那之前,我們要先來介紹一個東西「中斷」
這篇屬於比較進階的概念,知道 Effect 是怎麼進行排程的話比較可以了解當使用
Effect.fork等 function 時, Effect 是怎麼安排的,若是在測試流程時會有所幫助
Fiber.interruptFiber.interrupt 可以中斷正在執行中的 fiber ,這可以幫助我們進行接下來的實驗
import { Effect, Fiber, Console, pipe } from "effect"
pipe(
Effect.gen(function * () {
const fiber = yield * pipe(
Effect.gen(function * () {
yield * Effect.sleep('10 seconds')
yield * Console.log('after 10s')
}),
Effect.fork
)
const res = yield * Fiber.interrupt(fiber)
yield * Console.log(res)
}),
Effect.runPromise
)
你會發現上面這個程式很快就結束,並印出 interrupt 的錯誤。理解中斷機制,除了能讓我們在必要時終止執行中的 Effect 任務,更重要的是,它能幫助我們找出 Effect 的調度器何時介入我們的程式流程,進行任務安排
雖然在實務應用程式中直接使用
Fiber.interrupt的機會可能不多,但理解其運作方式對於 debugging 以及掌握 Effect 的 fiber 調度機制而言很重要
還記得前一篇我們提到的 JavaScript 的單執行緒的概念嗎? js 一次只能做一件事,如果我們正在做事,那 Effect 的排程就沒有機會做事,也就是說中斷的時機點,必須要是 Effect 的排程機制做事的時候,我們可以透過這個方法驗證一下, Effect 是不是在一開始說的時間點拿回控制權的,我們改一下前一個中斷的範例
import { Effect, Fiber, Console, pipe } from "effect"
pipe(
Effect.gen(function * () {
const fiber = yield * pipe(
Effect.gen(function * () {
console.log('1')
// 假設這是某種複雜的計算,且中間不會用到 Effect 的 code
for (let i = 0; i < 1000000; ++i) {}
console.log('2')
// 測試一下是不是在 yield 時交出控制權,如果是,那理論上就會在這邊中斷
yield * Effect.sleep('1 millis')
console.log('3')
}),
Effect.fork
)
// 主動先讓給 fork 的 fiber 執行,不然主要流程會優先執行
// 你可以試看看移除掉這個 yieldNow ,你會發現連 1 跟 2 都沒輸出
yield * Effect.yieldNow()
const res = yield * Fiber.interrupt(fiber)
yield * Console.log(res)
}),
Effect.runPromise
)
上面的程式你可以試著執行看看,沒意外應該會是輸出了 1 跟 2 後被中斷,也就是控制權交回給 Effect 做調度的時機點
再來我們實際來看一個 fiber 被實際使用的情況吧,當我們用 Effect.all 平行執行 Effect 時, Effect 會為了每一個 concurrency 開一個 fiber 進行執行,我們可以試著執行以下的程式碼看看
import { Array, Console, Effect, Fiber, pipe } from "effect"
pipe(
// 這個是 Effect 提供的一個好用的 helper function ,可以快速的建立 10 個元素的陣列
Array.makeBy(10, (i) =>
Effect.gen(function*() {
// 取得當下自己正在執行中的 fiber
// 有趣的是這個 getCurrentFiber 實際上回傳的是 Option 而非 Effect
// Effect 中有些資料型態本身也可以被當作 Effect 執行,像 Option 當作 Effect 執行時:
// - 若 Option 有值就會回傳那個值
// - 若 Option 沒有值就會產生 NoSuchElementException 錯誤
const fiber = yield* Fiber.getCurrentFiber()
// 取得 fiber 的 id
const fiberId = Fiber.id(fiber)
yield* Console.log(`This is #${i} Effect`, fiberId)
})),
// 這邊的 concurrency 可以試著調整一下觀察看看
Effect.allWith({ concurrency: 2 }),
Effect.runPromise
)
沒意外的話應該會看到 log 中的 fiber id 不是 2 就是 3 吧 (1 是主要的流程),這代表 Effect 開了兩個 fiber 在平行執行我們的任務
在這篇文章裡我們簡單的介紹了 Effect 底層是如何進行任務的調度的,這對於了解 Effect 是如何運作的會有所幫助,下一篇我們要來看 Effect 的一個內建的功能: batch request