纤程燃烧

288 阅读11分钟

前言

当男人从酒馆后面灰溜溜的离开的时候,他杂乱的胡髭上还沾着啤酒泡沫,另一边的嘴角乌青、粘着血迹。他把自己裹在棕色的皮革大衣里头,从路灯间的一个阴影走到另一个阴影。不知道究竟是从什么时候开始,他弄丢了自己的身份,就像他不知道自己的大衣是什么时候粘上那些永远洗不干净的油渍一样。人们第一次叫错他的名字的时候,他只感到新奇。他刚来到这座城镇,在这里房屋高高翘起的屋檐,高楼像是由喝醉了的建筑师构造出来一般相互缠绕着向高处伸展,高架桥在楼房之间胡乱穿行,庞然巨物般的列车行驶在一根钢笔粗细的轨道上。一切新奇的事物令他惊讶的说不出话,他猜想别人也被这琳琅满目的混乱扰的头昏脑涨,所以才记不住他的名字。当第二次有人叫错她的名字的时候,她满脸愠色的提出抗议。她严厉的语气涌向对方向两边摊开的手掌,却无力的跌到了地板上。再后来,没有人记得它真正的名字是什么,就像是有人用订书机将那个该死的名字订在了它的耳朵上一样。

他在阴暗马路的坑洞间摔倒,脑袋砸在正在腐烂的老鼠尸体上。冰冷的天气抑制了腐败的进程,只有当腥臭的脓液沾到身上的时候,他才注意到整条街道的地基都构建在死去之物的尸体上。它们的躯体交错溶解在一起,掩埋在浅浅的土层下。他惊魂不定的跑回到家中,紧紧关闭屋门试图将那可怖的景象锁在门外。他试图做一些事情转移自己的注意力,平息自己仍在震颤不已的神经。他拿起边上的书强迫自己读了一行在两周内发生三起火灾他不安的抬起头望着窗外的摩天大楼扭动着身躯发出朝着天空嘶吼他一只手捏着书页另一只手不停搓着指节他失去了自己的名字只剩下一个空洞的形象书页上的文字在他眼里逐渐模糊几乎要消失在灰暗的空气中负责人说,仅有几英亩的林地着火,这种事不亚于奇迹他感受到自己腹腔隐隐传来痉挛的疼痛传递到颅骨内部即将支离破碎的躯体被痛苦勉强捆绑在一起他站起身摸索着寻找火柴和蜡烛脚掌敲在地板上的声音在空荡的房间里反复回响点燃的烛火被黑暗挤压着几乎要熄灭他嘟囔着试图回想起自己的名字又一只蜡烛被点燃照亮了书页的一角不太可能再次发生这样的奇迹了他的嘴唇翕动喉咙却如酷暑中的河床般干涸影子在身后摇晃准备狩猎他残存的生命他将剩余的蜡烛全部点燃烛泪淌到腿上抓挠着他的皮肤他摇晃身体试图站起来却又隆隆倒下倾倒的烛火舔舐着书页逐渐向四周蔓延开来他想张开嘴巴喊


协程

在计算机科学中,例程 (Routine) 被定义为一系列操作的序列。例程的执行形成了一种父子关系,子例程总是在父例程之前终止。协程Coroutine, 这个术语由 Melvin Conway 引入)是例程的泛化(Donald Knuth)。协程和例程的主要区别在于,协程通过保留执行状态并提供额外的操作来显式地挂起和恢复其进度,从而提供了增强的控制流(维护执行上下文)。

In computer science routines are defined as a sequence of operations. The execution of routines forms a parentchild relationship and the child terminates always before the parent. Coroutines (the term was introduced by Melvin Conway) are a generalization of routines (Donald Knuth). The principal difference between coroutines and routines is that a coroutine enables explicit suspend and resume of its progress via additional operations by preserving execution state and thus provides an enhanced control flow (maintaining the execution context).

www.open-std.org/jtc1/sc22/w…

协程被实例化并调用。当调用者调用一个协程时,控制权会立即转移到该协程;当协程暂停时,控制权立即返回给其调用者。协程库不提供协程同步的方法:协程本身已经是同步的。 协程更像是一个普通函数,但具有语义扩展:将控制权传递给其调用者,并期望稍后在完全相同的点恢复。当调用者恢复一个协程时,控制权的转移是即时的。没有中央的调度器决定接下来恢复哪个协程。

A coroutine is instantiated and called. When the invoker calls a coroutine, control immediately transfers into that coroutine; when the coroutine yields, control immediately returns to its caller.

The coroutine library provides no facilities for synchronizing coroutines: coroutines are already synchronous.

A coroutine much more closely resembles an ordinary function, with a semantic extension: passing control to its caller with the expectation of being resumed later at exactly the same point. When the invoker resumes a coroutine, the control transfer is immediate. There is no intermediary, no agent deciding which coroutine to resume next.

www.open-std.org/jtc1/sc22/w…

JS 中的协程

我们可以通过协程实现某一部分程序的暂停与继续,从而协调多个不同的任务以实现异步的数据流。对应到 JavaScript 语言中,可以通过生成器语法来实现这一模式:

// refer to: https://lorenzofox.dev/posts/coroutine/

const co = (genFn) => (...args) => {
  const gen = genFn(...args);
    
  // no data to next as the routine has not been paused yet
  return next();

  function next(data) {
    const { value, done } = gen.next(data);

    if (done) {
      return value;
    }

    // non promise value
    if (value?.then === undefined) {
      return next(value);
    }

    // we resume the routine assigning the resolved value to "yield"  
    return value.then(next);
  }

};

const fn = co(function* (arg) {
  let value = yield asyncTask(arg);
  value = yield otherAsyncTask(value);
  return value;
});

fn(42).then(console.log);

上述的代码提供了一个通过协程对几个异步任务进行协调的示例。在 co 函数中,通过生成器函数 next 以及 promise.then,依次执行多个不同的异步任务。

事实上,从生成器函数的命名中我们也能够了解到它与协程之间的关系。在 Kotlin 中,yield 表达式是这样进行描述的: (Yield) 会暂停此协程并立即将其安排为后续执行。协程在一个线程上不间断地运行,直到协程挂起,从而使其他协程有机会使用该线程进行自己的计算。

Suspends this coroutine and immediately schedules it for further execution.

A coroutine run uninterrupted on a thread until the coroutine suspend, giving other coroutines a chance to use that thread for their own computations.

kotlinlang.org/api/kotlinx…

纤程

我们可以将“纤程”视为“用户空间线程”。纤程被启动后,从概念上讲,它可以拥有独立于启动它的代码的生命周期。纤程可以与启动代码分离;或者,一个纤程可以加入另一个纤程。纤程可以休眠直到指定的时间,或休眠指定的持续时间。多个概念上独立的纤程可以在同一个内核线程上运行。当一个纤程阻塞时,例如等待尚不可用的结果,同一线程上的其他纤程可以继续运行。阻塞一个纤程会隐式地将控制权转移给纤程调度器,以调度其他准备好运行的纤程。

We can regard the term ‘fiber’ to mean ‘user-space thread.’ A fiber is launched, and conceptually it can have a lifespan independent of the code that launched it. A fiber can be detached from the launching code; alternatively, one fiber can join another. A fiber can sleep until a specified time, or for a specified duration. Multiple conceptually-independent fibers can run on the same kernel thread. When a fiber blocks, for instance waiting for a result that’s not yet available, other fibers on the same thread continue to run. ‘Blocking’ a fiber implicitly transfers control to a fiber scheduler to dispatch some other ready-to-run fiber.

www.open-std.org/jtc1/sc22/w…

当一个纤程阻塞时,它不能假设调度器会在其等待条件满足的那一刻唤醒它。满足该条件标志着等待的纤程已准备好运行;最终,将会由调度器选择准备好的纤程进行调度

纤程和内核线程之间的关键区别在于,纤程使用协作式上下文切换,而不是抢占式时间片。 两个在同一内核线程上的纤程不会在不同的处理器核心上同时运行。在特定内核线程上的纤程中,最多只有一个纤程在任何给定时刻可以运行。

When a fiber blocks, it cannot assume that the scheduler will awaken it the moment its wait condition has been satisfied. Satisfaction of that condition marks the waiting fiber ready-to-run; eventually the scheduler will select that ready fiber for dispatch.

The key difference between fibers and kernel threads is that fibers use cooperative context switching, instead of preemptive time-slicing. Two fibers on the same kernel thread will not run simultaneously on different processor cores. At most one of the fibers on a particular kernel thread can be running at any given moment.

www.open-std.org/jtc1/sc22/w…

从概念上讲,纤程与协程最大的区别在于:纤程(Fiber)通过一个中央调度器执行上下文的切换,而协程(Coroutine)则不需要。

React 中的纤程

两个关键词:调度、阻塞。React 中的 Fiber 实现允许 React 以时间分片的形式执行一次昂贵的渲染动作。这一时间分片的实现在 React 中称为 Work Loop,其核心在于将一个完整的同步任务拆分为多个可以较小的分片任务,从而避免长时间的阻塞线程,导致 UI 渲染的卡顿:

A Cartoon Intro to Fiber (React Conf 2017)

在具体的实现中,React 中从根节点 HostRootFiber 开始基于具体的元素结构构建了一整棵 Fiber 树。Fiber 树结构与 ReactElement 的结构基本对应,描述了相应元素的执行任务:

7km.top/main/object…

以下简单列举了一些 Fiber 节点相关的属性(具体的属性内容可以参看 react-reconciler/src/ReactInternalTypes.js 文件中的声明):

export type Fiber = {
  // 标记 Fiber 类型
  tag: WorkTag,
  // 元素类型
  elementType: any,

  // 节点返回的目标(父节点)
  return: Fiber | null,

  // 链表结构
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,

  // 更新队列
  updateQueue: mixed,

  // 元素副作用
  nextEffect: Fiber | null,
  firstEffect: Fiber | null,
  lastEffect: Fiber | null,

  // 车道模型属性
  lanes: Lanes,
  childLanes: Lanes,

  // Fiber 树更新过程中对应的副本
  alternate: Fiber | null,

  // 其他属性
  ...attributes for debugging & timer
};

这一 Fiber 结构并不仅仅描述了 React 元素对应的状态,更重要的是,它是 Reconciler 阶段中的最小执行单元。React 中通过 WorkLoop 的中央任务调度器管理每一个 Fiber 的执行。

核心的调度函数实现实际上并不复杂,Reconciler 阶段会将 Fiber 对应的执行任务通过 scheduleCallback 进行调度。这一函数将依据判断从而决定将任务放入 timerQueue(延后执行) 或者 taskQueue(立即执行)

function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime();

  // 前述准备
  ... prepare startTime & timeout

  var expirationTime = startTime + timeout;

  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };

  // 如果任务的开始时间大于当前时间,应当放入 timerQueue 中
  if (startTime > currentTime) {
    // This is a delayed task.
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 否则放入 taskQueue
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    // Schedule a host callback, if needed. If we're already performing work,
    // wait until the next time we yield.
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

随后,在 WorkLoop (由 flushWork 调用)中会对 taskQueue 和 timerQueue 中的任务进行具体的执行。WorkLoop 的核心逻辑是一个 while 循环,它会不断地从 taskQueue 中取出任务进行执行,直到没有剩余时间后则会终止执行。此时如果队列中还有额外的任务,则会安排一个异步调用以触发下一次的 workLoop:

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  // 循环执行 task,直到满足条件才终止
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // 如果没有剩余时间则终止本次 workLoop 的执行
      // This currentTask hasn't expired, and we've reached the deadline.
      break;
    }
    const callback = currentTask.callback;
    // 如果包含回调函数则需要具体处理
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      // 执行具体的函数任务
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      const continuationCallback = callback(didUserCallbackTimeout);
      // 更新执行后的时间
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        currentTask.callback = continuationCallback;
      } else {
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      // 基于当前时间更新 timerQueue 中的任务状态,决定是否将其放入 taskQueue 中
      advanceTimers(currentTime);
    } else {
      // 无回调执行情况下直接移除任务
      pop(taskQueue);
    }
    // 取出一个新的任务
    currentTask = peek(taskQueue);
  }
  // 如果还有后续任务,则安排一次异步调用从而触发下一个 workLoop 执行
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

可以看到,整个渲染任务拆分为了每一个 Fiber 所对应的分片任务,并通过中央的 workLoop 进行调度,决定它们在什么时机下执行。这也就是纤程的设计理念。

闲话

标题引用自我的 2024 年度短篇小说榜单第一名《炽焰燃烧》。其中的《艰难时事》《炽焰燃烧》《上山路》等几篇都十分出色,罗恩拉什对隐匿在平静背后的躁动与痛苦刻画的极为生动。满分推荐。

参考资料