介绍
Garden Night 的入口检查现在能够正确分类响应,但最初的草稿会一次处理一项任务。缓慢的入口会推迟其他有用结果的返回。你将让少量固定数量的 Go 工作者分担任务,同时保留每项任务的结果。
这个独立单元已经提供完整的单目标检查函数。你应当已经了解 Go 函数、结构体、切片、模块、基本通道语法,以及前一个单元中的 HTTP 结果,或具有同等经验。goroutine、通道关闭和工作者结束协调都会在使用前讲解。所有请求都只访问准备好的本地目标;这是一次性工具,不是基准测试或监控平台。包括阅读和常见错误处理在内,预计需要约 60 分钟;这是作者估计。
先观察真实请求,再分担工作
本步骤运行当前的串行检查工具,查看服务端的实际观察记录。从 Workspace 开始。cd 选择项目目录;export 将准备好的 Go 目录加入当前 shell 的命令查找路径。使用新终端会话时,需要重新执行这两行。
cd /home/labex/project/site-checker
export PATH=/usr/local/go/bin:$PATH
读取检查函数、工作循环以及不会超时的任务列表。cat 只显示这些文件:
cat check.go run.go overlap.json
Check 已经能够返回 ok、http_error、content_mismatch、connection_error 或 timeout,保留任务身份和收到的状态码,并关闭每个正文。串行的 Run 忽略了它的 workers 参数。这就是你接下来要补全的行为。
overlap.json 中的入口每次都会执行有限的 250ms 准备工作。将每个目标的超时设置为两秒,就能让本次观察关注并发,而不是客户端超时。go run . 运行当前包;-input 选择任务,-workers 指定同时执行检查的上限,-timeout 2s 设置请求时间预算。> 将标准输出写入文件。
go run . -input overlap.json -workers 2 -timeout 2s > normal-report.json
读取报告:
cat normal-report.json
六项检查都应为 ok。但仅凭这份报告,无法知道它们是否发生重叠。准备好的观察器会在真实服务处理函数开始和结束时记录 enter 和 exit 事件;active 统计实际正在处理请求的函数数量。这个小型 Python 读取工具只取得已有观察记录,不会增加目标请求:
python3 observe.py
在这个全新的串行现场中,预期看到 Actual handler peak: 1;进入与退出交替发生,最多只有一个处理函数处于活动状态。观察器在这些请求发生之前就已就绪。有限的 250ms 工作为并发客户端提供了发生重叠的机会;延迟本身或总运行时长都不能证明重叠已经发生。真正的证据是事件记录。
现在的草稿能够生成完整报告,但同时执行的实际工作仍然最多只有一项。保留这份报告,作为本步骤的正常样例。下一步只替换 run.go;活动服务、目标定义和检查函数都会继续保留。
把任务分配给固定数量的工作者
本步骤先设计任务和结果如何流动,再在 Workspace 中编辑 Run。保持已完成的 Check 和预备输入不变。你的成果是能工作的工作池,而不只是几个已启动的 goroutine。
确定各通道由谁负责
goroutine 是与调用者并发运行的函数;go func() { ... }() 会立即启动匿名函数。启动它并不证明 HTTP 请求已经处于活动状态。通道传递特定类型的值:make(chan Target) 创建任务通道,make(chan Result) 创建结果通道。在这些无缓冲通道上,jobs <- target 发送一个值并等待接收者。for target := range jobs 持续接收,直到通道关闭且数据取完。close(jobs) 表示不会再发送新值;已收到的值仍然有效。
编辑前,先决定程序中哪一部分发送任务、接收任务、发送结果、接收结果,以及关闭各通道。应有一个任务生产者和一个结果收集者。工作者接收任务并发送结果。只有生产者知道何时不再发送任务,因此由它关闭 jobs。只要另一个工作者仍可能发送,任何工作者都不能安全关闭 results。
sync.WaitGroup 跟踪完成情况。var finished sync.WaitGroup 这样的声明从零开始。启动每个工作者之前调用 finished.Add(1);在工作者内部使用 defer finished.Done() 标记其退出。finished.Wait() 等待每个已登记工作者都调用 Done。只有该等待结束后,结果通道才能关闭。将等待和关闭的责任交给单独的 goroutine:如果主 goroutine 在接收之前等待,工作者可能会阻塞在结果发送上。生产者也单独运行,因此分配工作时,主 goroutine 仍可接收结果。
要启动指定数量的工作者,可使用 for i := 0; i < workers; i++ { ... } 这样的计数循环:将 i 初始化为零,每次迭代前检查上界,迭代后增加 i。循环体每次启动一个工作者。每个工作者必须同步调用 Check,发送其返回结果,然后才能接收下一项任务。这使活动客户端检查数量不超过工作者数量。不要在工作者内部为每个目标启动新 goroutine。共享一个带有 Timeout: budget 的 http.Client;该客户端支持并发请求。只有收集者向报告切片追加内容,因此工作者不会争用共享切片或 map。不同完成顺序都是合法的。
目标列表为空时,生产者仍会关闭 jobs。工作者随后结束,负责关闭的 goroutine 关闭 results,收集者就能返回一个已初始化的空切片。写代码前先追踪这个顺序。也要解释为什么三个工作者处理一个目标必须能结束,以及为什么一个工作者处理多个目标必须串行。
编辑一个连贯的工作循环
使用非 .go 扩展名备份文件,避免 Go 编译出第二个 Run 定义。cp 复制文件,不改变原文件:
cp run.go run.go.bak
打开已有文件:
nano run.go
用方向键移动,输入文字插入,使用 Backspace 或 Delete 删除字符。Ctrl 表示按住 Control 同时按另一个键。Ctrl+W、func Run、Enter 用于搜索函数;Ctrl+C 取消搜索。Ctrl+K 剪切当前行,因此仅在确实想删除该行时使用。按 Ctrl+O 保存,确认 run.go,再按 Enter;Ctrl+X 退出。要放弃意外修改,按 Ctrl+X,nano 询问是否保存时按 N。遇到不想使用的文件名提示,Ctrl+C 可取消,让你纠正编辑或保存到原文件名。
保留函数签名及已有 HTTP 客户端。使用 WaitGroup 时,在导入分组中添加 sync。用自己的设计替换串行目标循环,按以下顺序处理各项责任:
首先,创建有类型的 jobs 和 results 通道,以及用于工作者完成同步的 WaitGroup。登记并启动恰好 workers 个工作者。每个工作者持续接收直到 jobs 关闭,发送 Check 的每个结果,并在返回时标记完成。
接着,启动单独的生产者,发送每个目标,包括 URL 相同但 ID 不同的任务。最后一次发送后关闭 jobs,即使没有目标也要关闭。
另外启动单独的完成协调 goroutine,等待全部工作者,再关闭 results。只要工作者仍可能发送,就不能关闭 results。
最后,在主 goroutine 中初始化空结果切片,持续接收每个结果直到结果通道关闭,逐个追加,再返回切片。
运行前先写完这四个部分。缺少发送者、关闭者或收集者的不完整工作池,不是有用的中间试验。你需要决定各部分如何连接;这里不提供可粘贴的成品 Run。局部变量名或等价的同步选择不参与评分。
保存并退出,再检查实际文件。cat 只显示它,不修改内容:
cat run.go
gofmt -w 原地格式化源码。语法错误会指出文件和行号:重新打开 run.go,修复那一块、保存,再格式化。不要用新的整文件答案覆盖已完成部分。
gofmt -w run.go
检查真实的重叠工作
现在用两个工作者,对不会超时的任务运行完整工作池。两秒预算让其有限的 250ms 工作正常完成:
go run . -input overlap.json -workers 2 -timeout 2s
每项任务都应返回 ok 结果。读取观察器已有记录:
python3 observe.py
此场景的累计记录现在应显示 Actual handler peak: 2,其中一次 enter 发生时,另一个处理函数仍处于活动状态。如果峰值仍为一,检查工作者如何接收任务和调用 Check;总耗时不能证明成功。调度异常延迟可能使观察不足以下结论。一旦最新记录以 exit 和 active=0 结束,有限请求就已真正完成;可以重复这个无害批次并读取新事件。
累计峰值超过二,表示某个时间点发生过超限工作。早先错误实现留下的峰值,在修正源码后仍然保留。结合最新的进入/退出事件,并使用 Verify 判断当前实现;它会在独立受控服务中运行你的原样源码。不要清除历史或重置 VM 来抹去早先峰值。
如果命令一直卡住而不返回,Ctrl+C 会中断这次前台调用;独立准备的活动服务仍可使用。重新打开 run.go,检查通道责任链:全部任务是否已发送、生产者是否关闭 jobs、工作者是否调用 Done、它们发送时收集者能否接收、关闭者最终是否关闭 results?局部修复并格式化。开始下一次并发观察前,运行 python3 observe.py,确认最新记录事件为 exit 且 active=0。若有限请求仍活跃,继续读取直到记录满足条件;不要猜一个 sleep 时间,也不要删除观察记录。客户端超时不会立即终止服务端工作。
文件缺失时,用 pwd 和第一步的 cd 恢复目录。若新 shell 中无法使用 go,重复那一步的会话级 export。普通编辑或路径错误无需更改全局 shell 文件或重置 VM。
在各自的时间预算内完成全部检查
本步骤使用工作池生成活动报告,并检验它的边界。运行前先读取实际任务:
cat targets.json
预期的结果类型是两个 ok、一个状态码为 403 的 http_error、一个状态码为 200 的 content_mismatch、一个状态码为零的 timeout,以及一个状态码为零的 connection_error。票务服务的延迟是有限的 600ms;200ms 的客户端预算会有意提前结束这次检查。重复签到 URL 对应的两个 ID 都必须保留在报告中。
使用两个工作者保存新报告。> 只替换 report.json;错误仍会显示在终端中。
go run . -input targets.json -workers 2 -timeout 200ms > report.json
读取你实际保存的文件:
cat report.json
结果行的顺序可能与输入列表不同,不同运行之间也可能变化。按任务 ID 对照:一个慢请求可能比后面分配的快速请求更晚完成。单个工作者只有在 Check 返回后才会开始下一个目标,但客户端已经超时离开后,服务端处理函数仍可能继续执行有限工作。因此,课程在另一组不会超时的请求上测量工作者上限,不会把超时后仍在处理的服务端请求误判为工作池错误。
工作池还必须处理任务少于工作者,以及完全没有任务的情况。这些命令使用准备好的列表,并直接显示结果,不会改动 report.json:
go run . -input single.json -workers 3 -timeout 2s
go run . -input empty.json -workers 3 -timeout 2s
预期先出现一个 ok 结果,然后出现 [],两个命令都能结束。最后,对多个不会超时的目标请求上限为一:
go run . -input overlap.json -workers 1 -timeout 2s
所有结果仍然为 ok。尽管代码能够启动多个工作者,这次调用也必须串行处理。观察器的累计峰值保留了先前的活动,因此无法仅用它隔离判断本次调用的上限。课程检查会为每个工作者预算创建独立的新受控服务,包括一、二和三。对于预算为二或三的多目标批次,它要求观察到至少两个真实重叠请求,同时不超过请求的上限;并不要求峰值恰好为三。空批次和单目标批次没有重叠要求。
保留完整活动报告,以及原始输入和服务。现在,这个可复用命令既能限制同时执行的工作,又能交付每个目标的完整结果,包括如实报告的失败。
总结
你使用了职责明确的任务和结果通道、固定工作者数量,以及结束同步机制。单一收集者保留了每项任务的结果,避免并发修改共享切片。真实服务端事件区分了串行、有限并发和过量工作,而独立的超时测试避免把残留的服务端处理误判为工作者上限违规。即使部分入口失败,保存的报告仍然有用。



