test: 引擎核心的测试与基准
除了常规用例,有几组是专门钉住「引擎的既定事实」的,改动时会先红:
- vmstate_test goja 的硬限制:非原始值不是 goroutine 安全的、对象跨不了
Runtime。其中一条是哨兵——goja 哪天允许对象跨 Runtime 了,
它会失败,提醒我们可以简化设计。
- govalue_test Go 的切片/map 进到脚本里长什么样。这不是本库的行为而是
goja 的,但脚本作者照着它写代码,变了会静默走错分支。
- safety_test 并发下坏 VM 不会被别的 goroutine 捡到。
- bench_gobind 脚本碰 Go 对象的单次开销,README 性能一节的数据来源。
missing_global_test 里有一条断言 typeof setTimeout === "undefined":
定时器必须保持未定义,库里的特性探测才能正常降级,防止以后有人把桩加回来。
This commit is contained in:
+587
@@ -0,0 +1,587 @@
|
||||
package jscriptx_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
// 这一组直接用 goja,不经过本库的任何封装,钉住整个设计赖以成立的两条引擎事实:
|
||||
//
|
||||
// 1. Program 无状态,可以给任意多个 Runtime 共用——本库共用编译结果的前提
|
||||
// 2. **模块顶层的变量跟着 Runtime 走,不是共享状态**
|
||||
//
|
||||
// 第 2 条是最容易误解的地方。它导致:一个脚本串行开发时表现正常(一直命中同一个
|
||||
// Runtime),并发起来行为却随调度漂移——有几个 Runtime 就有几份独立的顶层状态,
|
||||
// 一次调用落到哪个 Runtime 取决于当时的 goroutine 调度。
|
||||
//
|
||||
// 所以要在并发之间共享状态,只能把它放到 Go 侧(写一个扩展,见 Extension 接口):
|
||||
// Go 对象可以被每个 Runtime 各自包一层,并发安全由 Go 自己保证。
|
||||
|
||||
const gojaCounter = `
|
||||
var counter = 0;
|
||||
function bump() { return ++counter }
|
||||
bump`
|
||||
|
||||
// compileCounter 编一份 Program。它是无状态的,几个用例共用同一份。
|
||||
func compileCounter(t *testing.T) *goja.Program {
|
||||
t.Helper()
|
||||
prog, err := goja.Compile("counter.js", gojaCounter, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return prog
|
||||
}
|
||||
|
||||
// bumpOf 在一个新 Runtime 里跑一遍 Program,拿到那个闭包函数。
|
||||
func bumpOf(t *testing.T, prog *goja.Program) goja.Callable {
|
||||
t.Helper()
|
||||
rt := goja.New()
|
||||
v, err := rt.RunProgram(prog)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fn, ok := goja.AssertFunction(v)
|
||||
if !ok {
|
||||
t.Fatalf("Program 的完成值不是函数: %v", v)
|
||||
}
|
||||
return fn
|
||||
}
|
||||
|
||||
func callBump(t *testing.T, fn goja.Callable) int64 {
|
||||
t.Helper()
|
||||
v, err := fn(goja.Undefined())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return v.ToInteger()
|
||||
}
|
||||
|
||||
// 同一个 Runtime 反复调用,顶层变量一路累加——闭包还在,Program 不会重跑。
|
||||
func TestGoja_同一个Runtime里顶层变量会累加(t *testing.T) {
|
||||
fn := bumpOf(t, compileCounter(t))
|
||||
|
||||
for i := int64(1); i <= 5; i++ {
|
||||
if got := callBump(t, fn); got != i {
|
||||
t.Fatalf("第 %d 次调用该拿到 %d,拿到 %d", i, i, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 换一个 Runtime 就是另一份顶层变量,哪怕用的是同一个 Program。
|
||||
// 这正是「N 个 VM = N 份独立副本」的根源。
|
||||
func TestGoja_不同Runtime各有一份顶层变量(t *testing.T) {
|
||||
prog := compileCounter(t)
|
||||
|
||||
a := bumpOf(t, prog)
|
||||
b := bumpOf(t, prog)
|
||||
|
||||
for range 3 { // a 先数到 3
|
||||
callBump(t, a)
|
||||
}
|
||||
if got := callBump(t, b); got != 1 { // b 是全新的
|
||||
t.Errorf("另一个 Runtime 该从 1 开始,拿到 %d", got)
|
||||
}
|
||||
if got := callBump(t, a); got != 4 { // a 不受影响
|
||||
t.Errorf("原来那个 Runtime 该接着数到 4,拿到 %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Program 可以并发给任意多个 Runtime 用。32 个各调一次,全都拿到 1
|
||||
// ——32 份独立的顶层状态,互不影响。
|
||||
func TestGoja_Program可以并发共用(t *testing.T) {
|
||||
prog := compileCounter(t)
|
||||
|
||||
var mu sync.Mutex
|
||||
var got []int64
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for range 32 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
rt := goja.New()
|
||||
v, err := rt.RunProgram(prog) // 同一份 Program,不同 Runtime
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
fn, ok := goja.AssertFunction(v)
|
||||
if !ok {
|
||||
t.Error("完成值不是函数")
|
||||
return
|
||||
}
|
||||
r, err := fn(goja.Undefined())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
got = append(got, r.ToInteger())
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
fmt.Println(got)
|
||||
|
||||
if len(got) != 32 {
|
||||
t.Fatalf("该有 32 个结果,拿到 %d 个", len(got))
|
||||
}
|
||||
for _, v := range got {
|
||||
if v != 1 {
|
||||
t.Fatalf("每个 Runtime 都是新的,该全是 1,出现了 %d;全部结果 %v", v, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 对象不能跨 Runtime 传。这是 goja 的硬限制,也是「JS 内部变量没法并发共享」的根据。
|
||||
//
|
||||
// 维护者在 dop251/goja#232 里说得很直白:非原始类型的 goja.Value 连并发只读都不安全,
|
||||
// 用 Proxy 包一层也不行,因为 handler 最终还是要访问原来那个值。
|
||||
//
|
||||
// 这条测试是个哨兵:哪天 goja 放开了这个限制,它会失败,提醒我们重新评估
|
||||
// 「共享状态必须放 Go 侧」这个结论。
|
||||
func TestGoja_对象不能跨Runtime传(t *testing.T) {
|
||||
rt1 := goja.New()
|
||||
obj, err := rt1.RunString(`var o = { n: 1 }; o`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rt2 := goja.New()
|
||||
setErr := rt2.Set("borrowed", obj) // 把 rt1 的对象塞进 rt2
|
||||
_, runErr := rt2.RunString(`borrowed.n`)
|
||||
|
||||
if setErr == nil && runErr == nil {
|
||||
t.Fatal("跨 Runtime 传对象居然成功了——goja 的限制变了," +
|
||||
"本库关于「共享状态只能放 Go 侧」的结论需要重新评估")
|
||||
}
|
||||
t.Logf("Set 的结果: %v", setErr)
|
||||
t.Logf("读取的结果: %v", runErr)
|
||||
}
|
||||
|
||||
// 原始值不受这条限制——字符串、数字、布尔不是对象,可以跨 Runtime 传。
|
||||
func TestGoja_原始值可以跨Runtime传(t *testing.T) {
|
||||
rt1 := goja.New()
|
||||
v, err := rt1.RunString(`"hello"`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rt2 := goja.New()
|
||||
if err := rt2.Set("s", v); err != nil {
|
||||
t.Fatalf("原始值该能传: %v", err)
|
||||
}
|
||||
got, err := rt2.RunString(`s + " world"`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.String() != "hello world" {
|
||||
t.Errorf("拿到 %q", got.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 并发之间要真正共享一个计数器(1,2,3… 不重不漏)该怎么做
|
||||
// ============================================================================
|
||||
|
||||
// sharedCounter 是放在 Go 侧的计数器,并发安全由它自己保证。
|
||||
// 每个 Runtime 各自把它包一层——这是 goja 维护者认可的唯一共享方式。
|
||||
type sharedCounter struct {
|
||||
mu sync.Mutex
|
||||
n int64
|
||||
}
|
||||
|
||||
func (c *sharedCounter) Bump() int64 {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.n++
|
||||
return c.n
|
||||
}
|
||||
|
||||
// 方案一:状态放 Go 侧,Runtime 各自独立。**推荐这条。**
|
||||
//
|
||||
// 32 个 goroutine 各有各的 Runtime(互不阻塞),但 Set 进去的是同一个 Go 对象,
|
||||
// 所以 Bump() 拿到的是同一个计数器:结果正好 1..32,不重不漏。
|
||||
//
|
||||
// 只有 Bump 里那把 Go 锁是串行的,脚本执行本身全程并行。
|
||||
// 把它包成扩展之后,脚本侧写 store.Incr("counter", 1) 就行——本仓库的
|
||||
// internal/testext 是个最小示例,框架层那份在 framework 的 internal/jscript/ext/store。
|
||||
func TestGoja_共享状态放Go侧(t *testing.T) {
|
||||
const n = 32
|
||||
|
||||
prog, err := goja.Compile("shared.js", `
|
||||
function bump() { return shared.Bump() }
|
||||
bump`, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
counter := &sharedCounter{} // 进程级,只有一个
|
||||
got := runConcurrent(t, n, func() (int64, error) {
|
||||
rt := goja.New()
|
||||
if err := rt.Set("shared", counter); err != nil { // 同一个指针,各包一层
|
||||
return 0, err
|
||||
}
|
||||
v, err := rt.RunProgram(prog)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
fn, ok := goja.AssertFunction(v)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("完成值不是函数")
|
||||
}
|
||||
r, err := fn(goja.Undefined())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.ToInteger(), nil
|
||||
})
|
||||
|
||||
fmt.Println("方案一 共享 Go 对象:", got)
|
||||
assertSequence(t, got, n)
|
||||
}
|
||||
|
||||
// 方案二:共用一个 Runtime,靠锁串行。
|
||||
//
|
||||
// 这样 JS 里的 counter 是真共享的——因为只有一个 Runtime。代价是**所有脚本执行
|
||||
// 完全串行**:脚本跑 1ms,32 个并发就是 32ms 串起来。
|
||||
//
|
||||
// 除非脚本极快且并发很低,否则别走这条。状态放 Go 侧更安全,也不用牺牲并发。
|
||||
func TestGoja_共用一个Runtime靠锁串行(t *testing.T) {
|
||||
const n = 32
|
||||
|
||||
prog, err := goja.Compile("locked.js", `
|
||||
var counter = 0;
|
||||
function bump() { return ++counter }
|
||||
bump`, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rt := goja.New()
|
||||
v, err := rt.RunProgram(prog)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fn, ok := goja.AssertFunction(v)
|
||||
if !ok {
|
||||
t.Fatal("完成值不是函数")
|
||||
}
|
||||
|
||||
var rtMu sync.Mutex // Runtime 不是并发安全的,这把锁不能省
|
||||
got := runConcurrent(t, n, func() (int64, error) {
|
||||
rtMu.Lock()
|
||||
defer rtMu.Unlock()
|
||||
r, err := fn(goja.Undefined())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.ToInteger(), nil
|
||||
})
|
||||
|
||||
fmt.Println("方案二 共用 Runtime:", got)
|
||||
assertSequence(t, got, n)
|
||||
}
|
||||
|
||||
// runConcurrent 并发跑 n 次 do,收集结果并排序。
|
||||
func runConcurrent(t *testing.T, n int, do func() (int64, error)) []int64 {
|
||||
t.Helper()
|
||||
|
||||
var mu sync.Mutex
|
||||
out := make([]int64, 0, n)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for range n {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
v, err := do()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
out = append(out, v)
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// assertSequence 断言结果正好是 1..n,不重不漏。
|
||||
func assertSequence(t *testing.T, got []int64, n int) {
|
||||
t.Helper()
|
||||
|
||||
if len(got) != n {
|
||||
t.Fatalf("该有 %d 个结果,拿到 %d 个: %v", n, len(got), got)
|
||||
}
|
||||
for i, v := range got {
|
||||
if v != int64(i+1) {
|
||||
t.Fatalf("该是 1..%d 的连续序列,第 %d 个是 %d;全部结果 %v", n, i+1, v, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 脚本里 +1 之后阻塞会怎样:两个方案在这里差别最大
|
||||
// ============================================================================
|
||||
|
||||
const blockOnce = 100 * time.Millisecond
|
||||
|
||||
// sleeper 暴露给脚本的阻塞函数,模拟脚本里做 IO(查库、调接口)。
|
||||
func sleeper(d time.Duration) func() { return func() { time.Sleep(d) } }
|
||||
|
||||
// 方案一:计数器在 Go 侧,Runtime 各自独立。
|
||||
//
|
||||
// 脚本先 +1 拿到自己的号,再阻塞。16 个各睡 20ms,总耗时接近 20ms 而不是 320ms
|
||||
// ——一个脚本卡在 IO 上,不挡别人。而且 +1 在阻塞之前就已经对其他人可见了。
|
||||
func TestGoja_阻塞时独立Runtime并行(t *testing.T) {
|
||||
const n = 16
|
||||
|
||||
prog, err := goja.Compile("blocking.js", `
|
||||
function bump() {
|
||||
var mine = shared.Bump(); // 先 +1,拿到自己的号
|
||||
sleep(); // 再阻塞
|
||||
return mine
|
||||
}
|
||||
bump`, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
counter := &sharedCounter{}
|
||||
sleep := sleeper(blockOnce)
|
||||
start := time.Now()
|
||||
|
||||
got := runConcurrent(t, n, func() (int64, error) {
|
||||
rt := goja.New()
|
||||
if err := rt.Set("shared", counter); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := rt.Set("sleep", sleep); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
v, err := rt.RunProgram(prog)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
fn, ok := goja.AssertFunction(v)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("完成值不是函数")
|
||||
}
|
||||
r, err := fn(goja.Undefined())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.ToInteger(), nil
|
||||
})
|
||||
|
||||
elapsed := time.Since(start)
|
||||
fmt.Printf("方案一 独立 Runtime: %d 个各阻塞 %v,总耗时 %v\n",
|
||||
n, blockOnce, elapsed.Round(time.Millisecond))
|
||||
|
||||
fmt.Println(got)
|
||||
|
||||
assertSequence(t, got, n)
|
||||
|
||||
if limit := blockOnce * n / 2; elapsed > limit {
|
||||
t.Errorf("独立 Runtime 该并行,总耗时 %v 超过了 %v——像是串行了", elapsed, limit)
|
||||
}
|
||||
}
|
||||
|
||||
// 方案二:计数器就在 JS 里,共用一个 Runtime。
|
||||
//
|
||||
// 脚本先 counter++(JS 侧的变量,真共享),再阻塞。同样 16 个各睡 20ms,
|
||||
// 总耗时接近 320ms:Runtime 不是并发安全的,锁必须一直持到脚本返回,
|
||||
// **脚本在 IO 上卡多久,锁就被占多久**,包括 +1 之后那段。
|
||||
//
|
||||
// 所以"共用 Runtime 换 JS 侧共享状态"的代价不是慢一点,是并发归零。
|
||||
func TestGoja_阻塞时共用Runtime串行(t *testing.T) {
|
||||
const n = 16
|
||||
|
||||
// 导出两个方法:最后一个表达式是什么,RunProgram 就返回什么。
|
||||
// 想导出多个就返回一个对象——注意外面要包一层括号,
|
||||
// 不然 { 会被当成语句块而不是对象字面量。
|
||||
prog, err := goja.Compile("blocking.js", `
|
||||
var counter = 0;
|
||||
function bump() {
|
||||
counter++; // 先 +1,这是 JS 里的变量,共用 Runtime 才共享得了
|
||||
sleep(); // 再阻塞
|
||||
return counter
|
||||
}
|
||||
function read() { return counter } // 不 +1,只读当前值
|
||||
({ bump: bump, read: read })`, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rt := goja.New()
|
||||
if err := rt.Set("sleep", sleeper(blockOnce)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v, err := rt.RunProgram(prog)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exports := v.ToObject(rt)
|
||||
fn, ok := goja.AssertFunction(exports.Get("bump"))
|
||||
if !ok {
|
||||
t.Fatal("bump 不是函数")
|
||||
}
|
||||
read, ok := goja.AssertFunction(exports.Get("read"))
|
||||
if !ok {
|
||||
t.Fatal("read 不是函数")
|
||||
}
|
||||
|
||||
var rtMu sync.Mutex
|
||||
start := time.Now()
|
||||
|
||||
got := runConcurrent(t, n, func() (int64, error) {
|
||||
rtMu.Lock()
|
||||
defer rtMu.Unlock() // 锁持到脚本返回,包括 +1 之后阻塞的那段
|
||||
r, err := fn(goja.Undefined())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.ToInteger(), nil
|
||||
})
|
||||
|
||||
fmt.Println(got)
|
||||
|
||||
elapsed := time.Since(start)
|
||||
fmt.Printf("方案二 共用 Runtime: %d 个各阻塞 %v,总耗时 %v\n",
|
||||
n, blockOnce, elapsed.Round(time.Millisecond))
|
||||
|
||||
assertSequence(t, got, n)
|
||||
|
||||
if floor := blockOnce * n * 4 / 5; elapsed < floor {
|
||||
t.Errorf("共用 Runtime 必然串行,总耗时才 %v,不到 %v——是不是没锁住", elapsed, floor)
|
||||
}
|
||||
|
||||
// 全部并发结束后读最终值。读也要加锁——它跟 bump 用的是同一个 Runtime。
|
||||
rtMu.Lock()
|
||||
final, err := read(goja.Undefined())
|
||||
rtMu.Unlock()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Printf(" 并发结束后 read(): %d\n", final.ToInteger())
|
||||
|
||||
if got := final.ToInteger(); got != int64(n) {
|
||||
t.Errorf("%d 个并发各 +1 一次,最终该是 %d,读到 %d", n, n, got)
|
||||
}
|
||||
}
|
||||
|
||||
// 对照:独立 Runtime + JS 侧的 counter++。
|
||||
//
|
||||
// 跟"共用 Runtime"那条用的是**完全相同的脚本**,只有 Runtime 的用法不同。
|
||||
// 结果:并行是并行了(~20ms 而不是 320ms),但 16 个全拿到 1
|
||||
// ——每个 Runtime 一份自己的 counter,压根没共享。
|
||||
//
|
||||
// 这条和前两条合起来是个 2×2:
|
||||
//
|
||||
// JS 侧 counter Go 侧 counter
|
||||
// 独立 Runtime 全是 1,并行 ✗ 1..16,并行 ✓
|
||||
// 共用 Runtime 1..16,串行 ✗ (没必要,白白串行)
|
||||
//
|
||||
// 只有右上角那格两头都占。
|
||||
func TestGoja_独立Runtime里的JS计数器不共享(t *testing.T) {
|
||||
const n = 16
|
||||
|
||||
// 跟 TestGoja_阻塞时共用Runtime串行 用的是同一段脚本
|
||||
prog, err := goja.Compile("blocking.js", `
|
||||
var counter = 0;
|
||||
function bump() {
|
||||
counter++;
|
||||
sleep();
|
||||
return counter
|
||||
}
|
||||
function read() { return counter }
|
||||
({ bump: bump, read: read })`, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sleep := sleeper(blockOnce)
|
||||
|
||||
// 把每个 Runtime 的 read 留下来,跑完再各读各的
|
||||
var readMu sync.Mutex
|
||||
var readers []goja.Callable
|
||||
|
||||
start := time.Now()
|
||||
|
||||
got := runConcurrent(t, n, func() (int64, error) {
|
||||
rt := goja.New() // 每个 goroutine 一个 Runtime
|
||||
if err := rt.Set("sleep", sleep); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
v, err := rt.RunProgram(prog)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
exports := v.ToObject(rt)
|
||||
fn, ok := goja.AssertFunction(exports.Get("bump"))
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("bump 不是函数")
|
||||
}
|
||||
read, ok := goja.AssertFunction(exports.Get("read"))
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("read 不是函数")
|
||||
}
|
||||
|
||||
r, err := fn(goja.Undefined())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
readMu.Lock()
|
||||
readers = append(readers, read)
|
||||
readMu.Unlock()
|
||||
|
||||
return r.ToInteger(), nil
|
||||
})
|
||||
|
||||
elapsed := time.Since(start)
|
||||
fmt.Printf("对照 独立 Runtime + JS 计数器: %v 总耗时 %v\n",
|
||||
got, elapsed.Round(time.Millisecond))
|
||||
|
||||
// 并行了
|
||||
if limit := blockOnce * n / 2; elapsed > limit {
|
||||
t.Errorf("独立 Runtime 该并行,总耗时 %v 超过了 %v", elapsed, limit)
|
||||
}
|
||||
// 但没共享:每个 Runtime 自己的 counter 都是第一次 +1
|
||||
for _, v := range got {
|
||||
if v != 1 {
|
||||
t.Fatalf("每个 Runtime 一份 counter,该全是 1,出现了 %d;全部结果 %v", v, got)
|
||||
}
|
||||
}
|
||||
|
||||
// 全部结束后各读各的:16 个 Runtime,16 份 counter,每份都是 1。
|
||||
// 对比共用 Runtime 那条——那边最终读到 16。
|
||||
//
|
||||
// 每个 Runtime 只有创建它的那条路径碰过,此刻也没人在跑,所以读不用加锁。
|
||||
finals := make([]int64, 0, len(readers))
|
||||
for _, read := range readers {
|
||||
v, err := read(goja.Undefined())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
finals = append(finals, v.ToInteger())
|
||||
}
|
||||
fmt.Printf(" 并发结束后各自 read(): %v\n", finals)
|
||||
|
||||
for i, v := range finals {
|
||||
if v != 1 {
|
||||
t.Errorf("第 %d 个 Runtime 的 counter 该是 1,读到 %d", i, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user