---
description: 进程模块 —— 进程阻塞、停止与继续执行问题
---

# Stuck & Stop & Continue

在之前粗糙的设计中，也不是没有问题，有一些细节需要敲定。

在操作系统中，因为比较底层，所以进程的PCB中会记录当前的PC，可以严格控制进程的执行时间，以及执行的汇编指令位置。每一条汇编指令都是⚛️原子指令。

在 Screeps 中我们做不到这一点，正如我在序言中所言，我们希望汲取的是设计理念，而不是严格遵照原本的设计，所以可以适当的做一些适应性改变。

而且其实也没必要，我们有更丰富的高级语言特性，不是不可以模拟，只是对于一个进程我们可能就要写这样的代码：

```typescript
const process = [
    {"func": func1, "params": func1_params}, // equivalent to Line 1
    {"func": func2, "params": func2_params}, // equivalent to line 2
    ..., 
]
```

### ⚛️ 原子函数

每一个函数都类似一个⚛️原子指令，我们称之为⚛️**原子函数**，这样可以在任意函数位置阻塞、停止或者是恢复了，但是这样写的有些过于复杂了，不符合简约的设计理念。

{% hint style="info" %}
**Good to know**: 采用类似💻汇编的思路来编写进程的具体内容，是包含了🔄有限状态机的表示形式的。换言之，🔄有限状态机是💻汇编思路的子集。
{% endhint %}

### ✋ 主动停止

首先，我想引入✋主动停止这个概念，就是进程在执行的过程中，可以**主动暂停执行**，让渡给另一个进程。

而不向正常操作系统那样，都&#x662F;_&#x88AB;动停止_，在时间片结束后，暂停执行。实际上，我不想引入时间片&#x548C;_&#x88AB;动停&#x6B62;_&#x7684;概念，也不是不可以实现，一方面，计时就是一个耗时过程，另一方面，区别于正常计算机运行，在 Screeps 中是以 tick 为时间单位的，换句话说就是主频很低，可能只有 0.1 Hz🤣，一个进程通常需要在一个 tick 中完整的执行来实现某个功能整体。

如果一个功能整体分散到几个 tick 中，耗时就有些过于长了。把一个 tick 拆成多个 slot 其实感觉不太有必要。

{% hint style="warning" %}
**注意**: ✋主动停止是区别于🛑阻塞的，🛑阻塞是要等待恢复才可以继续运行，✋主动停止则表示当前 tick 执行提前结束，但未完成，下一 tick 仍然需要执行。
{% endhint %}

### 🤔 必要 … 吗？

那么进程的✋停止、🛑阻塞与✔️恢复执行有没有必要呢？实际上进程的✔️恢复执行非常有利于**一个有依赖关系的任务的顺序执行**。

设想这样一个情景，我们需要某个 _Creep_ 从 A 拿一些东西 送到 B，如果是采用每 tick 重复执行的方式的话，我们就需要在每次重复执行时，判断 Creep 的状态来决定现在是处于"到 A 的路上", "取东西", "到 B 的路上" 还是 "放东西"的阶段。但是，这四个状态是一个接着一个的，在完成第一个状态之后，完全可以直接过渡到第二个状态，没必要再进行检查。如果我们把这四个过程分为四个进程，又显得有些浪费资源，因为一个进程会伴随着一些相关变量和空间的分配，往往是比较大的。那么✋停止与✔️恢复执行就显得十分有必要了这里。

那么该怎么写比较优雅？同时我们之前也只是涉及了顺序执行，没有考虑到条件控制以及循环结构。这里就要用到 JavaScript 的闭包等高级特性。

### 💻 进程流程设计（仿汇编）

首先，为了美观，我们写一个⚛️原子函数的时候，可以包装成一个匿名函数，也就是：

```typescript
() => func.call(...params)
```

其次，我们可以设置一些全局变量，作用类似于状态寄存器，例如：

```typescript
let flag1 = null;
let flag2 = false;
```

最后，在每个⚛️原子函数的内部，我们可以完成状态的更新。只要再支持标签和条件跳转就可以了。

标签非常容易，我们可以支持两种形式的⚛️原子函数定义：

```typescript
() => func.call(...params) // 第一种: 没有标签
["tag", () => func.call(...params)] // 第二种: 带标签，附在该原子函数前
```

而条件跳转则需要特殊处理，它由（条件跳转关键字，布尔判断函数，为真跳转的标签）三元组构成，假设为假的话，则自动执行下一条原子函数，它的定义就是：

```typescript
["JUMP", () => ..., "tag"]
```

因此，标签不能为"**JUMP**"，"**JUMP**"是保留字。

那么✋停止，🛑阻塞，✔️恢复执行的基础就有了。我们可以给一个进程分配一个PC来记录运行到了哪里，但是究竟该怎么具体✋停止、🛑阻塞呢？因为我们在执行的时候，本质上是在不断进行函数的嵌套调用，而栈和堆是有上限的，所以在进程🛑阻塞或者是✋停止后，是需要返回到原本的调用的地方，再进行判断中止。

### 📢 原子函数的返回值

事实上，我们可以利用⚛️原子函数的返回值来达到这一目的。

首先是主动停止，我们可以定义一个预先的返回值📢`OK_STOP` 表示正常执行，但是下面不再继续执行，进程中止。实际上，我们可以细分为三种：

* 📢`OK_STOP_CURRENT` 表示下一次执行时，仍然从当前原子函数重复执行
* 📢`OK_STOP_NEXT` 表示下一次执行时，从当前原子函数的下一条进行执行
* 📢`OK_STOP_CUSTOM` ，附带一个`tag` ，表示下次从`tag` 处接着执行

而如果返回📢`OK` ，则表明下面的在该 tick 可以继续执行。

而对于🛑阻塞，因为🛑阻塞的发生通常是在获得某个锁、信号量或者是管程的时候。我们不应该给予普通⚛️原子函数权限来🛑阻塞某个进程，而应该只针对某些特定函数开放权限。所以锁、信号量和管程都应该是做在操作系统内部的，进程拿到某个 Id，然后通过 OS 提供的接口函数来操作。而在 OS 内部的接口函数中就可以访问私有成员变量和函数，来调整进程状态。

{% hint style="danger" %}
**注意**: 这类锁、信号量或者是管程的操作函数应当返回两种返回值📢`OK` 或者是📢`STOP_STUCK`。操作系统在执行进程的每个⚛️原子函数结束后，如果碰到返回值是📢`STOP_STUCK`都应当再检查进程的状态后，再决定是否将进程转入阻塞队列。再进行校验检查的原因，是在于遵守&#x4E86;_**”不给犯错的机会“**_&#x7684;原则，如果只凭借📢`STOP_STUCK` 返回值，就决定将进程转入阻塞队列的话，会让普通原子函数有机会伪造返回值来让进程进入阻塞状态，并且无法被唤醒了。
{% endhint %}

此外，还应当考虑到❌错误的情况，即进程在运行当中遇到了错误。在这种情况下，应当是进程自己在返回错误`STOP_ERR`之前释放资源。而返回错误之后，有两种选择：销毁进程或者是重启进程。出于简化设计的目的，先暂定可以无限重启进程，从而保证进程会一直运行直到工作量结束，即一个进程承担的工作量是可以保证一定会完成的。

### 🔄 完整的生命周期

Okay，那么一个进程的完整的生命周期都应当定义完成了。

操作系统的原语提供进程创建，进程进入就绪状态 -> 操作系统从就绪队列中，逐个执行就绪进程 -> 进程执行，直到主动中止、阻塞或者是结束。

* 对于主动中止的进程，更改PC，放回就绪队列。
* 对于阻塞的进程，检查阻塞状态，放到阻塞队列中，等待唤醒。
* 对于结束的进程，销毁进程号，结束。

那么，对于一个经典的任务 ”从A拿一些东西到B“ 就可以描述成：

```typescript
function issueTransferProc<U extends StoredStructure, V extends StoredStructure>(
    creepId: Id<Creep>, // 哪个 Creep 承担该项任务
    src: Id<U>, 
    resouceType: ResourceType, 
    amount: number, 
    dest: Id<V>, 
): number { // 返回进程号
    // 定义一些 Flags
    // ... (在这个任务当中不需要)
    
    // 定义一些全局变量
    // ... (在这个任务当中不需要)
    
    // 定义原子函数
    
    // 前往 A
    function gotoSrc() {
        const creep = Game.getObjectById(creepId);
        const src = Game.getObjectById(src);
        if (!creep || !src) return STOP_ERR; // 错误, 结束
        
        // 这里的移动比较暴力, 不能体现出我们主动中断的优势。
        // 实际上，在加上了路径优化的模块之后，我们是可以精确
        // 计算什么时候 Creep 能够移动到目标的，然后释放信号
        if (creep.pos.roomName === src.pos.roomName && 
            creep.pos.getRangeTo(src) <= 1) return OK; // 进入下一个原子函数
        
        creep.moveTo(src); // 在有路径优化模块后，不再通过 moveTo 原生方法调用
                           // 而是通过 OS 的与 Creep 对接的驱动程序
        return OK_STOP_CURRENT;
    }
    
    // 拿东西
    function withdrawSrc() {
        const creep = Game.getObjectById(creepId);
        const src = Game.getObjectById(src);
        if (!creep || !src) return STOP_ERR; // 错误, 结束
        
        // 这里在引入资源管理之后，不应当失败。
        // 所以在失败时，应当进行错误处理。
        if (creep.withdraw(src, resourceType, amount) !== OK) return STOP_END; // 错误, 结束
        
        return OK; // withdraw 和 move 可以在同一 tick 执行
    }
    
    // 前往 B
    function gotoDest() {
        const creep = Game.getObjectById(creepId);
        const dest = Game.getObjectById(dest);
        if (!creep || !dest) return STOP_ERR; // 错误, 结束
        
        if (creep.pos.roomName === dest.pos.roomName && 
            creep.pos.getRangeTo(dest) <= 1) return OK; // 进入下一个原子函数
        
        creep.moveTo(dest);
        return OK_STOP_CURRENT;
    }
    
    // 放东西
    function transferDest() {
        const creep = Game.getObjectById(creepId);
        const dest = Game.getObjectById(dest);
        if (!creep || !dest) return STOP_ERR; // 错误, 结束
        
        if (creep.transfer(dest, resourceType, amount) !== OK) return STOP_ERR; // 错误, 结束
        
        return OK; // 正常结束
    }
    
    return Apollo.proc.create([
        gotoSrc, 
        withdrawSrc, 
        gotoDest, 
        transferDest, 
    ]);
}
```

这个示例目前还非常的粗糙，例如`creep`，`src`，`dest`都是指定的，那么在出现错误之后，进程重启仍然会错误。科学的做法，应当是动态申请`creep`和资源。随着后面我们其他模块的引入，我们会不断地来优化他。;D
