Go 15:启动 goroutine 后谁等待它结束
原来的顺序 CLI读取文本后,统计、生成任务记录、输出结果依次发生。把 analyzeTask 前面加上 go,看似只改一个关键字,却把调用方原本拥有的“等统计结束”这项责任删掉了:go analyzeTask(...) 不会把两个返回值交回当前函数,也不会等统计结束。于是问题不只是如何启动计算,而是怎样知道可以安全地写出 SUCCEEDED。 本章仍修改同一累计工程的 CLI:每项任务的统计交给一个 goroutine,调用者显式等待该项工作发出完成信号,然后继续沿用先前的错误判断和记录逻辑。完整累计工程和 SHA-256 校验附在正文素材目录;正文所述的实际实验命令、双流和退出码在 writing-plans/go-language/evidence/15/。这里没有把每个编号做成独立项目。 独立复跑步骤列出本章正反例、源码校验及运行命令。 启动信号不足以发布结果 先看独立反例 testdata/async-without-wait.go。计算者启动后关闭 started,随后卡在 <-release。主流程观察到 started,却没有放行计算者;此时查看 done,...
Python 31:await 让出了什么
一个词频任务已经提交,调用 run_async(store, task_id) 后查询仍是 queued;把返回值交给 asyncio.create_task,紧接着查询也还是 queued。任务究竟什么时候开始?这与 09 的生成器惰性相似,但协程由事件循环驱动,不能把 yield 和 await 混作同一种业务调度。 本篇沿用 27 的词频工作负载和 submit → run → query → cancel 状态合同,仅增加 run_async 入口。创建协程对象不运行其函数体;create_task 将其安排到事件循环运行,并返回可以等待的 Task。安排不等于当前这一行已经执行。测试紧接着查询时仍为 queued,给事件循环一次运行机会后,协程调用已有的 _start 进入 running。 能让出循环的等待 run_async 在 _start 后等待 asyncio.Event。未设置事件时,Task 暂停,另一个已经安排的心跳任务运行,词频任务仍是 running,没有结果。事件设置后它恢复,计算与提交结果走现有 app.count_words、app._com...
Python 30:多进程多出的成本在哪里
把同一段 "One two one" 和同样的 cpu_work(..., iterations=20000) 交给顺序入口与进程池入口,两者的任务结果都是 {"one": 2, "two": 1}。进程池回传的工作者 PID 与父进程不同,子进程的解释器路径仍是 /usr/bin/python3.12。但把本地 lambda 当参数送进进程池时,结果 Future 报 Can't pickle local object;父任务被记为 failed,没有成功结果。多进程能并行运行独立解释器,并不意味着它能共享任意 Python 函数或当前进程里的对象。 29 的线程池通过共享同一个进程里的任务对象协作;本篇只添加进程池执行策略,没有更换提交、执行、查询、取消的业务规则。进程启动方式明确选 spawn,不能把当前 Linux 上别的默认设置推广给所有平台。 明确送过去的是什么 ProcessTaskRunner 用 ProcessPoolExecutor(mp_context=multiprocess...
Python 29:线程池什么时候结束工作
线程池只有一个工作者,第一条任务已进入27 的受控等待,第二条排队。第三条提交立即得到 BufferError: thread queue is full。取消排队的第二条时 Future.cancel() 返回 True,任务状态成为 cancelled;取消正在运行的第一条时 Future.cancel() 返回 False,但业务层接受了取消请求,状态暂为 cancel_requested。门闩还没释放时,第一条仍在运行;释放后它才完成当前工作单元、停止提交成功结果并变成 cancelled。 这条时间线刻意分开三种事实:Future 是否还能从队列移除、应用是否已接收取消请求、工作者是否已经真正停止。拿一个 True 返回值覆盖三者,会让停机检查误判未结束的工作。 有界的不只是线程数 tasklab.thread_runner.ThreadTaskRunner 用 ThreadPoolExecutor(max_workers=1) 限制工作者数,用 BoundedSemaphore(workers + backlog) 同时约束运行与等待中的 Future 数。这里演示...
Python 28:GIL 能保护业务状态吗
两个线程都读取到同一任务是 queued,各自停在屏障前,等两者都到齐后继续写 running。两边的领取记录各有一次相同的 task-0001:列表写入本身即使没有损坏,**“检查状态 → 标记领取”**的复合业务动作也完成了两次。这是在00 固定的普通 CPython 3.12.3 环境下,由屏障确定时序得到的反例,不依赖盲目多跑几千次“碰巧复现”。 27 的同一任务输入仍是 "One two one"。本篇只把领取动作单独抽出来观察;故意错误的领取函数放在实验文件中,不接管正式 tasklab 执行入口。正式 SQLite 仓储仍从同一 app.run 计算词频,并按 21 的事务规则回写结果。 屏障放在检查与更新之间 反例里每个线程先通过 query(store, task_id) 看见 queued,再等 threading.Barrier(2),随后直接设置 running 并记录一次领取。屏障保证两边在任何一边更新之前都已经作出“可以领取”的判断。实验记录 claims == ["task-0001", "task...
Python 27:程序到底在等什么
给同一段 "One two one" 分别加纯 Python 算术循环和等待门闩,两种任务最终仍返回 {"one": 2, "two": 1}。在等待门闩的测试中,工作者已进入执行入口,主线程查询到 running,但工作者尚未完成;主线程释放事件之后,才读到 succeeded。两种执行过程都可能占用墙钟时间,却不是同一种资源压力。只把“这次花了多少毫秒”当作瓶颈结论,会把等待与计算混在一起。 第23 篇的受控检查点为超时定义了状态,第21 篇的 SQLite 仓储定义了提交规则。本篇只给既有顺序 app.run 和 SQLite 适配器加可选的同一工作负载钩子,不更改提交、查询和取消的业务结果;后续线程、进程和 asyncio 均需拿这套输入与计算路径对照。 两种成本不要合写成“执行慢” tasklab.workloads.cpu_work(text, iterations=N) 在 Python 循环里逐字符更新校验和,输入可固定、迭代次数可控,返回的校验和只是负载观测值,不是任务的词频结果。...
Go 14:有限输入和 JSON 输出的资源边界
13 之前的 CLI 用 io.ReadAll 读取文件:一个不受控制的大文件会先进入内存,再做任务统计。引入 stdin 与 JSON 后,读取量、坏 JSON 和 stdout 部分写出还会把原来的「输入有效才写结果」契约拆成几个环节。哪一层负责关闭文件?Writer 只接受了部分字节,程序还能声称调用没有任何输出吗? 同一累计工程本章新增 --stdin、--input-json 与 --json。--file/--stdin 的单份输入最多 65536 字节;--input-json 只允许搭配这两种来源,解码形如 {"text":"Go 写 Go"} 的 JSON。--json 对统计结果输出 JSON 数组,--list-ids 则输出字符串数组。已有 --text、--batch、--lookup 和顺序任务记录不改写成另一个工程。本章完整包与SHA256SUMS可在网站独立下载并复跑。 独立复跑步骤列出本章正反例、源码校验及运行命令。 超限必须多读一个字节 readBounded(input) 在 io.LimitReade...
Python 26:安装通过为何不等于发布通过
第25 篇生成的 wheel在 CPython 3.12.3 的干净 venv 能运行 tasklab 命令。但用 pip 的目标版本选项要求按 Python 3.11 解析同一 wheel,得到的是 requires a different Python: 3.11.0 not in '>=3.12',退出 1。这条反例不需要假装机器装了 3.11:它证明打包元数据会在解析阶段排除 3.11,却不能证明一个真实 3.11 解释器的运行表现。版本声明、解析结果和已经安装的环境应分开记录。 本篇只检查本地制品及发布前材料,不推送 Git、不上传 PyPI、不部署博客。所谓发布成功至少要有索引上传和可下载的外部制品记录;本地安装正常不能替代这些证据。 给同一制品三种身份 pyproject.toml 声明项目版本 0.1.0、支持 >=3.12、运行依赖为空;这些是作者写在构建输入中的约束。tools/audit_release.py 读取实际 wheel 的 METADATA 和 WHEEL,又对照 sdist 的 PKG-INFO:两边名称和版本一致,Require...
Go 13:泛型约束能替结果集合省下什么
12 之后,顺序任务服务的每条任务记录仍保存在 []TaskRecord 中。默认批量输出要把这些记录变成统计摘要;早先结果还可以直接持有 []TaskResult。是否值得为这种变换加一个泛型函数?先检查调用方希望保留哪种输入切片,再检查函数体真正要用什么操作,而不是把整套任务模型都改成泛型。 同一累计工程本章新增 Summaries[T HasSummary],主 CLI 的默认输出直接调用 Summaries(records)。查询、文件输入、错误分类、panic 注入测试和临时任务 ID 都仍在原工程;本篇完整累计归档包含正反命令与历史 Go 文本证据,SHA256SUMS提供下载校验。 独立复跑步骤列出本章正反例、源码校验及运行命令。 约束只承诺一个方法 1234567891011type HasSummary interface { Summary() string}func Summaries[T HasSummary](records []T) []string { lines := make([]string, len(...
Go 12:panic 时清理和 recover 的边界
11 已把空文本、查不到任务与文件读取失败变成可判断的错误。统计函数真的发生 panic 时,前面通过 defer 注册的清理还会执行吗?另一个常见误用是在普通代码中调用 recover(),以为它能给任何后续 panic 提供兜底。这两件事有不同条件,不能凭一次 go test 无失败就宣称资源安全。 本章继续沿用同一累计工程的 analyzeTask、readAndClose 与 CLI。analyzeTask 通过 analyzeTaskUsing(text, max, analyze) 将分析函数作为参数交给一个窄的测试接缝,生产入口仍传 stats.Analyze。panic 由测试替身注入;正式 CLI 没有安装吞掉未知 panic 的全局 recover。完整工程、正反原始证据及归档校验值随本篇素材包提供。 独立复跑步骤列出本章正反例、源码校验及运行命令。 先确认注入真的抵达统计函数 TestPanicsAndCleanup 传给 analyzeTaskUsing 一个会执行 panic("injected statistics panic") ...










