iT邦幫忙

2025 iThome 鐵人賽

DAY 18
0

這篇我們要來看 Effect 是怎麼調度我們的程式的執行流程的,如果你之前有碰過一些比較偏向系統程式的東西,你可能會知道, fiber 指的是協程,意即一種需要執行中的任務主動放棄處理器的控制權,來達成的排程機制

而 Effect 也實作了 fiber 的這個概念,在 js 的程式中將任務進行排程。到這邊你可能會想,我們寫程式一直都在執行,什麼時候主動放棄過控制權的?

答案是當你在 Effect.gen 使用 yield 時或是在 pipe 中,從一個 Effect 切換到下一個 Effect 的中間,這些時間點都會導致控制權回到 Effect 的調度器,讓調度器有機會安排如何執行 Effect

我們接下來可以來看一個例子,不過在那之前,我們要先來介紹一個東西「中斷」

這篇屬於比較進階的概念,知道 Effect 是怎麼進行排程的話比較可以了解當使用 Effect.fork 等 function 時, Effect 是怎麼安排的,若是在測試流程時會有所幫助

Fiber.interrupt

Fiber.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
)

(playground link)

你會發現上面這個程式很快就結束,並印出 interrupt 的錯誤。理解中斷機制,除了能讓我們在必要時終止執行中的 Effect 任務,更重要的是,它能幫助我們找出 Effect 的調度器何時介入我們的程式流程,進行任務安排

雖然在實務應用程式中直接使用 Fiber.interrupt 的機會可能不多,但理解其運作方式對於 debugging 以及掌握 Effect 的 fiber 調度機制而言很重要

Effect 排程的時機點

還記得前一篇我們提到的 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
)

(playground link)

上面的程式你可以試著執行看看,沒意外應該會是輸出了 1 跟 2 後被中斷,也就是控制權交回給 Effect 做調度的時機點

Effect.all 與 fiber

再來我們實際來看一個 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
)

(playground link)

沒意外的話應該會看到 log 中的 fiber id 不是 2 就是 3 吧 (1 是主要的流程),這代表 Effect 開了兩個 fiber 在平行執行我們的任務

在這篇文章裡我們簡單的介紹了 Effect 底層是如何進行任務的調度的,這對於了解 Effect 是如何運作的會有所幫助,下一篇我們要來看 Effect 的一個內建的功能: batch request


上一篇
16. 再看 concurrency:使用 fork 在背景執行
下一篇
18. Request and batching
系列文
Effect 魔法:打造堅不可摧的應用程式22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言