现象:源码/测试版(python -m src.main)语音播报流程流畅;pyinstaller 发行包卡在「播报文稿生成 ~38%」,进度与超时均失效。
结论:不是业务算法差异,而是冻结进程内引擎的启动方式触发了 windows 默认 proactoreventloop + 日志锁竞争,并叠加 gui 进程代理继承等问题。
1. 现象对照
| 维度 | 开发 / 测试版 | 打包发行版(修复前) |
|---|---|---|
| 启动入口 | python -m src.main | 桌面客户端程序.exe(sys.frozen=true) |
| 播报引擎进程模型 | 独立子进程:engine/.venv/.../python -m uvicorn app.main:app | 同进程守护线程:桌面壳内 threading.thread + uvicorn |
| 事件循环 | 子进程默认策略,外网断开路径相对独立 | 线程内 asyncio.run → 默认 proactoreventloop |
| 典型卡住点 | 无 | 任务中心显示「播报文稿生成 · …」约 38%,可挂死十余分钟 |
| 超时是否生效 | 正常 | 不生效(阶段 420s 超时、httpx 超时也无法打断) |
| 日志 | engine/engine.out.log 等 | engine-data/engine.log;挂死时常停在某条 [vo.normalize] / 大模型调用前后 |
前端 38% 来自播报阶段序号映射:script_storyboard 在有效步骤序列中约对应 3/8,即「播报文稿 agent 生成中」阶段,不是业务写死的 38 这个魔法数。
2. 架构分叉(问题入口)
桌面通过 produceengine.ensure_started() 拉起引擎,分支如下:
ensure_started() ├── frozen == false → 子进程 uvicorn(开发路径) └── frozen == true → _ensure_started_inprocess()(打包路径)
对应代码:src/produce_engine.py。
选择进程内线程的初衷合理:发行包无 .venv,依赖已打进 _internal,避免再附带解释器。副作用是:引擎与无控制台 gui、webview2、同步日志写文件共享同一进程地址空间,异步运行时行为与「独立 uvicorn」不再等价。
3. 问题定位过程
3.1 业务侧排除
- 同一数据库/缓存服务、大模型密钥配置下,开发版可完成同类型播报,说明 大模型、对象存储、库表本身可用。
- 卡住任务 db 状态长期为
running,progress_message停在「播报文稿 agent 生成中…」,completed_at为空。 - 阶段超时常量
stage_timeout_script_storyboard = 420存在,但挂死后 远超 420s 仍不失败 → 更像是 事件循环本身不再调度,而非单纯「模型慢」。
3.2 卡住热路径
播报文稿阶段主链:
pipeline._run_script_storyboard
→ agent_plan.generate_script_and_storyboard
→ search(httpx)
→ llm_client.chat_json(httpx)
→ finalize_voiceover_storyboard_sync(to_thread)
→ 进度心跳 _with_progress_heartbeat
日志中常见停顿点:[vo.script] step=llm_generate / [vo.normalize] dedupe begin 等。停在日志语句附近,提示 写日志与 asyncio 回调交叉,而不一定是 normalize 算法复杂度。
3.3 uvicorn 在 windows 上的关键行为
uvicorn.server.run() 大致为:
config.setup_event_loop() asyncio.run(self.serve(...))
uvicorn.loops.asyncio.asyncio_setup 源码(uvicorn 0.34):
def asyncio_setup(use_subprocess: bool = false) -> none:
if sys.platform == "win32" and use_subprocess:
asyncio.set_event_loop_policy(asyncio.windowsselectoreventlooppolicy())
而 config.use_subprocess 仅在 reload 或 workers > 1 时为真。
桌面进程内单 worker、无 reload:setup_event_loop() 等于空操作。
python 3.12 on windows 默认策略 → asyncio.run() 创建 proactoreventloop。
开发版:独立 python -m uvicorn 子进程,即使也是 proactor,也 不与 gui 主线程、同一套 rotatingfilehandler 高频争用;连接关闭异常路径的危害面更小,表现为「测试 ok」。
3.4 修复后的验证证据
新包启动后 engine-data/engine.log 出现:
冻结引擎 asyncio.run loop_factory -> _windowsselectoreventloop 引擎事件循环: _windowsselectoreventloop (selector=selectselector frozen=true)
说明发行路径已强制 selector;此前仅 set_event_loop_policy 再调用 server.run(),仍可能被 asyncio.run() 按默认工厂建出 proactor。
4. 根本原因
4.1 主因:proactor + 冻结 gui 线程内引擎 → 异步死锁式挂起
因果链:
- 打包版引擎跑在桌面进程的守护线程里。
- uvicorn 未因
use_subprocess切换 selector,asyncio.run使用 proactoreventloop。 - 播报阶段大量短连接(大模型对话、搜索、对象存储 head/get 等);远端关闭连接时,proactor 管道回调
_call_connection_lost可能在回调里抛出oserror(如 winerror 10038)。 - asyncio 默认异常处理器走
logging;同时业务 worker / 心跳线程高频写engine.log(文件 handler)。 - logging handler 锁与异常再入写日志叠加,可导致 业务协程永久卡在 logging 调用上;事件循环无法继续调度 → httpx 超时、
asyncio.wait_for超时全部失效。 - ui 仍按 stage 显示「播报文稿生成 ~38%」,表现为「打包就卡、测试不卡」。
这与「算法在打包后变慢」无关,是 运行时模型差异。
4.2 次因:gui 进程继承系统代理
无控制台 pyinstaller 进程常带上用户/机器级 http(s)_proxy。
httpx 默认 trust_env=true 时会走异常代理,外网调用表现为无限挂起,同样容易停在播报文稿阶段。
开发子进程环境变量集合往往不同,故不易复现。
对策(已落地):httpx_client_kwargs = {trust_env: false, proxy: none},并在冻结引擎线程入口清理代理环境变量、设置 no_proxy=*。
4.3 伴随问题(放大「卡」的体感,但非 38% 主因)
| 问题 | 表现 | 说明 |
|---|---|---|
| 删除项目未释放内存队列 | 新任务显示「排队等待(第 2 位)」 | 只删 db 行,未 cancel 同类型 asyncio.lock 等待者;计数泄漏 |
发行包漏打 aiomysql | 「启动播报引擎失败: no module named ‘aiomysql’」 | 壳与引擎共用冻结解释器,根 requirements.txt 未对齐引擎依赖 |
httpx 未进 collect_all | 偶发导入/运行异常 | 清单已补齐 |
5. 修复方案(已实现)
5.1 强制 selector 事件循环(主修复)
在 _ensure_started_inprocess 中 不再依赖 server.run() / uvicorn 的 setup_event_loop,改为:
- 设置
windowsselectoreventlooppolicy; asyncio.run(serve(), loop_factory=policy.new_event_loop),保证真正创建的是 selector 循环;- lifespan 日志打印
frozen与 loop 类型;若仍为 proactor 则打 error,便于回归。
5.2 代理隔离
- 全局 httpx:
trust_env=false+proxy=none - 冻结线程:剥离
http_proxy/https_proxy/all_proxy等
5.3 删除与队列
delete_series前调用task_queue.abandon_series_tasks,取消内存 worker,避免假排队。
5.4 打包依赖
- 根
requirements.txt对齐引擎(含aiomysql、httpx等); package_manifest.py将相关包列入collect_all/required_internal。
6. 为何「测试 ok、打包就卡」可一句话概括
测试版引擎是独立进程;打包版引擎挤进 gui 同进程线程,且误用 windows proactor 事件循环。连接关闭与日志争用把 asyncio 卡死,于是播报文稿阶段永远停在约 38%,超时也救不回来。
次要因素(代理、假排队、缺依赖)会叠加失败模式,但与「流畅 vs 卡死」二分最对齐的,是 进程模型 + 事件循环类型 这一条。
7. 回归检查清单
打包后启动,确认:
dist/.../engine-data/engine.log含_windowsselectoreventloop且frozen=true。- 执行播报任务后,
progress_message能在数分钟内越过「播报文稿 agent 生成中」,或在超时后变为失败而非永久 running。 - 删除进行中项目后,新建任务不应无故「第 2 位」排队。
verify_package.py通过,且_internal含aiomysql、httpx。
8. 相关代码与文档索引
| 路径 | 作用 |
|---|---|
src/produce_engine.py | frozen 分支、selector loop_factory、清代理、安全日志 handler |
engine/app/main.py | 启动时打印事件循环类型;proactor 告警 |
engine/app/services/volc/http_utils.py | httpx 禁用环境代理 |
engine/app/services/task_queue.py | 同类型串行锁、abandon_series_tasks |
engine/app/services/pipeline.py | delete_series 先放弃队列再删库 |
scripts/package_manifest.py / requirements.txt | 发行依赖对齐 |
docs/packaging-notes.md | 打包结构与分发注意 |
9. 后续建议(可选)
- 长期:评估打包版也改为「附带嵌入式 python / sidecar uvicorn」,与开发模型一致,降低同进程耦合。
- ci:冻结冒烟用例——启动 exe 后读
engine.log,断言 selector +/api/health。 - 可观测性:卡住时在任务中心展示「无心跳超过 n 分钟」并允许一键失败重置(前端已部分具备「无活跃任务则推进失败」逻辑)。
以上就是python webview打包版卡死、开发版正常的解决方法的详细内容,更多关于python webview打包版卡死开发版正常的资料请关注代码网其它相关文章!
发表评论