# 并发 Deft 提供三种并发原语,它们共享单个每运行时调度器。每种原语针对不同模式进行了调整: ## 原语 | 原语 | 适用场景 | |---|---| | deftask | 默认在单独线程上运行产生值的计算;`@stream` 用于增量结果。 | | defworker | 持久工作进程或池 —— 在长生命周期子运行时中加载文件,拥有完整模块访问权限(参见 defworker)。 | | defcoro + spawn | 具有自己事件循环的协作式长生命周期进程。 | | after / after-cancel | 一次性定时器。 | `defer`(在控制流中介绍)在函数退出时提供清理,并且能与这三种原语很好地组合。 ## `deftask` — 异步值 `deftask` 在全新的 OS 线程上运行其函数体,并带有自己的运行时(闭包的字节码被深拷贝,它引用的全局数据值被快照),返回一个 future。没有工作进程池 —— 每次调用都会派生一个新线程。`await` 阻塞直到 future 解析完成并产出其值。 函数体必须自包含:原生标准库函数可用,但捕获的局部变量和用户定义函数不会进入子运行时。只能使用字面量、全局数据和标准库函数 —— 从外层函数捕获的局部变量(比如 `$url`)传过去会变成 `nil`。 (如需完整的模块访问权限 —— 用户定义函数、导入、持久状态 —— 请使用 defworker。)
# Global data referenced by the body is snapshotted into the child runtime.
def api-base "https://api.example.com"

def f1 [deftask { [await [http/get "$api-base/a"]] }]
def f2 [deftask { [await [http/get "$api-base/b"]] }]
set results [await-all @{$f1 $f2}]
`@parallel` 可作为相同行为的显式标记:
def f [deftask { @parallel; [await [http/get "$api-base/a"]] }]
模式装饰器放在函数体顶部 —— 每个占一行,或用 `;` 分隔(`{ @stream body }`、`{ @stream @buffer-size 128 body }`)。 `await` 接受一个 future —— 或多个:`[await $f1 $f2 ...]` 依次阻塞每个 future,并返回值的列表(按顺序)。对于 future 列表,`await-all` 是 `stdext/async` 的包装器,它依次解析每个 future,并跳过非 future 项:
from "deft/stdext/async" import await-all
### `@stream` — 增量结果 对于长时间运行的生产者,用 `@stream` 标记函数体会产出值的流,而不是单个最终结果。生产者运行在自己的 OS 线程上;使用 `@buffer-size N` 设置流缓冲区大小(默认 64)。关于 `task/stream-*` 系列,参见标准库。
def s [deftask {
    @stream
    @buffer-size 128
    yield 1
    yield 2
    yield 3
}]
set row [task/stream-next $s]
while ![nil? $row] {
    # ... process $row ...
    set row [task/stream-next $s]
}
# or just: def rows [task/stream-collect $s]
### 错误语义 任务函数体内抛出的异常如何显现取决于模式: - `deftask` / `@parallel` —— future 以错误字符串解析(例如 `"boom (line 1)"`),`await` 将其作为任务的值返回。它不会在 await 处重新抛出。 - `@stream` —— 流直接结束;错误被吞掉。
set v [await $f]
if [is? "string" $v] {
    # task body threw: $v is the error message
}
## `defcoro` — 协作式协程 协程是拥有自己事件循环的长生命周期进程。使用 `spawn` 启动一个协程,用 `vwait` 驱动它。
def state [ref %{ :count 0 }]

defcoro worker {|name^string|
    while ([deref $state]~count < 3) {
        swap! $state { |s| assoc $s :count ($s~count + 1) }
        echo "$name tick"
        yield --wait 500
    }
}

set co [spawn [$worker "alpha"]]
[vwait]                                      # drives all coroutines on this runtime
### `yield` `yield` 挂起协程并将控制权交还给调用者。可选的 `--wait ms` 标志使其挂起至少 `ms` 毫秒:
defcoro ticker {||
    set n 0
    while true {
        incr n
        echo "tick $n"
        yield --wait 1000
    }
}
### `resume` `resume` 唤醒已让出的协程。调度器会在 `vwait` 内为你调用它;你很少需要直接调用。 ### `coro-state` `[coro-state $co]` 返回 `:ready`、`:running`、`:suspended`、`:done` 或 `:failed` 之一。 ## `after` — 一次性定时器 `after ms fn` 安排 `fn` 在 `ms` 毫秒后运行。返回一个可取消的定时器 id:
set id [after 5000 { echo "five seconds elapsed" }]

after-cancel $id                               # cancel the pending timer
定时器在运行时的事件循环上触发 —— 不需要单独的线程。 ## `sleep` `sleep ms` 至少阻塞当前协程/任务 `ms` 毫秒,期间将控制权让给其他任务。在普通脚本中(没有调度器运行)它只是阻塞。
sleep 100
## `defer` — 退出时清理 `defer` 在异步代码中的工作方式与在同步函数中相同:块在外层函数退出时运行,无论以何种方式退出。这使它非常适合释放异步资源:
def with-connection {|url^string|
    set conn [net/connect $url]
    defer { net/close-conn $conn }

    # ... use $conn — it'll be closed whether we return normally,
    # throw, or the task is cancelled ...
    [do-stuff $conn]
}
完整的 `defer` 参考参见控制流。 ## 每运行时调度器 每个运行时恰好有一个协程调度器。`deft` 脚本二进制驱动一个运行时;每个 TUI 应用面板在自己的 OS 线程中运行自己的运行时(参见运行时)。 由此带来以下结果: - 同一运行时上的协程直接共享状态;无需锁。 - `deftask` 的函数体始终在单独的子运行时中运行(闭包被快照,全局变量被深拷贝)—— 通过函数体传入数据,并通过 future/stream 取回结果。 - 不同运行时上的长生命周期进程必须使用 `pub/*`(参见发布/订阅)或 `rpc`(参见 RPC)进行通信。 ## 决策矩阵 | 需求 | 使用 | |---|---| | 在前台继续运行时将慢操作放到后台 | `deftask { ... }` + `await` | | 对 N 个输入进行并发映射 | `deftask { ... }`(自包含函数体)+ `await-all` | | 具有完整模块访问权限的持久工作进程(用户函数、导入、状态) | `defworker` + `worker/send` | | 长生命周期后台进程 | `defcoro` + `spawn` + `vwait` | | 一次性延迟动作 | `after ms fn` | | 周期性动作 | 在循环中使用 `yield --wait ms` 的 `defcoro` | | 跨运行时消息传递 | `pub/publish` + `pub/subscribe` | | 跨运行时直接调用 | 对 `@rpc` 函数使用 `rpc` | ### 为什么用 `defworker` 而不是 `deftask`? `deftask` 将闭包的字节码及引用的全局变量快照到一个全新的一次性运行时中:函数体必须自包含(不能调用用户定义函数,不能使用捕获的局部变量),且工作进程完成一个任务后即终止。`defworker` 将文件加载到位于自己 OS 线程上的持久子运行时中 —— 该文件的完整模块图可用,标记为 `@worker` 的 def 是其处理器,状态跨任务持久保留,处理器错误会解析为 `%Err` 而不会杀死工作进程。完整参考参见 defworker。 ## 快速参考 | 形式 | 用途 | |---|---| | `[deftask { ... }]` | 在新 OS 线程上运行函数体,返回 future | | `[deftask { @parallel ... }]` | 默认行为的显式形式 | | `[deftask { @stream ... }]` / `@buffer-size N` | 增量结果流 | | `[defworker "path.dft"]` / `[defworker "path.dft" n]` | 持久工作进程 / 池(参见 defworker) | | `[worker/send $w "name" args...]` | 向工作进程分派任务;返回 future | | `[worker/stream $w "name" args...]` | 流式任务;处理器中的 `yield` 推送到返回的流中 | | `[await $future]` | 阻塞等待 future 的值 | | `[await $f1 $f2 ...]` | 依次阻塞每个 future,返回值的列表 | | `await-all` (from stdext/async) | 阻塞等待 future 列表 | | `defcoro name { ... }` | 声明协程 | | `spawn $coro-or-closure` | 启动协程 | | `[vwait]` | 驱动此运行时上的所有协程 | | `yield` / `yield --wait ms` | 挂起 | | `[coro-state $co]` | 查看状态 | | `[after ms fn]` / `[after-cancel id]` | 一次性定时器 | | `sleep ms` | 阻塞当前任务 |