
程序启动时卡住不动——没有报错、没有崩溃,进程在那里,界面不出来。
遇到这种现象,第一反应通常是"系统有问题"或者"硬件不行"。但有一类启动卡死,根子在某个模块的初始化代码里:它在加载期间做了不该在那里做的事,把自己锁住了。
这篇讲清这个机制。它属于"DLL修复工具 完全无能为力"的一类问题——因为文件一个都不缺。
一个模块被加载,要经过几步:映射进地址空间、修正地址引用(重定位)、解析它依赖的模块、最后执行各模块自己的初始化例程。
最后这一步是关键:初始化例程是在"加载器锁"的保护下执行的。
加载器锁的作用是保护加载器内部的数据结构——模块列表、依赖关系、已加载模块的状态。只要有人在加载模块,这些结构就可能被改动,所以必须串行化。
而这个锁是进程级的:所有线程共享同一把。
理解这一点,后面三条硬约束就都成立。
约束一:不要在初始化例程里装载别的模块。
装载模块要再次进入加载器——而你自己正拿着那把锁。如果这次装载需要另一个线程配合,就进入死锁:你在等其他线程,而其他线程在等那把锁。
约束二:不要在初始化例程里创建线程并等待它。
新线程一旦需要加载模块(包括隐式的依赖解析),就会被锁挡住。主线程在等新线程,新线程在等锁,锁在主线程手里——典型死锁。
约束三:不要在初始化例程里做耗时操作。
初始化期间锁是被持有的,所有其他加载请求都被挡在门外。所以在这里读文件、访问网络、做大量计算,不只是"自己慢",而是"整个进程的加载都被拖住"。
这三条的共同表现就是开头那句:卡住不动,不报错也不崩溃——因为线程在等锁,而不是在崩溃。
死锁的成立依赖线程交错:两个线程恰好以某个顺序各自拿到一半资源,才会僵持。
这意味着大多数情况下它侥幸通过——这也是这类 bug 最讨厌的地方:
提高复现率的办法:增加启动阶段的并发、多挂几个模块、在资源紧张的机器上跑。它不会自己消失,只会越来越容易触发。
即使没有死锁,还有一条同样容易踩的坑:
多个模块之间的初始化顺序,除依赖关系之外并不被保证。
于是"在 A 的初始化里假设 B 已经初始化完毕"是一种脆弱设计。在开发机上可能恰好成立(加载顺序比较固定),换了机器或换了模块集合就不成立。
结论:不要把跨模块的依赖放在初始化阶段,推迟到首次实际使用时。
初始化例程里只做最必要、不涉及外部依赖的事:设置内部变量、注册自己的回调。不要在其中装载模块、创建线程、访问文件或网络。
需要做更多的事,就推迟到首次使用时(惰性初始化)。示意:
// keep module init trivial; defer heavy work to first use
static std::once_flag initFlag;
void EnsureInitialized() {
std::call_once(initFlag, []() {
// load dependencies and initialize resources here
});
}惰性初始化要注意线程安全:用"一次初始化"的语义(示例里的工具正是这个用途),保证并发调用时只执行一次。
真正需要在进程启动时做的事,放在程序自己的入口里做——那时候加载阶段已经结束,锁已经释放。
三条件同时成立,基本可以确认(这也是 DLL修复工具 在这里帮不上忙的判断依据):
如果有调试手段,看一眼各线程的调用栈:多个线程停在加载相关的调用上,就说明是在等同一把锁。
这类问题和"缺文件"类故障的表现完全不同:缺文件会报错、会弹窗、会直接退出;加载死锁是静静地不动。所以它不需要 DLL修复工具 去补什么,需要的是定位到那个做了危险初始化的模块。
关于"启动卡死"这一类,记住四条:
对使用者来说,可用的判断很朴素:不报错、不崩溃、就是不动,且去掉某个新组件后就正常——这三点凑齐,方向就基本确定了。而这类故障与文件是否齐备无关,所以修补类工具在这里派不上用场。
https://www.ijinshan.com/functions/repairdll.html?channel=4071
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。