为啥 Windows 10 在我的程序中启动额外线程?

Posted

技术标签:

【中文标题】为啥 Windows 10 在我的程序中启动额外线程?【英文标题】:Why does Windows 10 start extra threads in my program?为什么 Windows 10 在我的程序中启动额外线程? 【发布时间】:2016-04-21 16:42:55 【问题描述】:

使用 Visual Studio 2015,在一个新的空 C++ 项目中,为控制台应用程序构建以下内容:

int main() 
    return 0;

在返回时设置断点并在调试器中启动程序。在 Windows 7 上,截至断点,该程序只有一个线程。但在 Windows 10 上,它有五个(!)线程:主线程和四个等待同步对象的“工作线程”。

谁在启动线程池(或者我如何找到)?

【问题讨论】:

关于 Windows 和无限循环的笑话来了…… 可能进程在 Windows 10 上默认获取线程池。 我首先在CreateThread 上设置一个断点。请注意,使用 windbg 按名称放置断点非常常见,而在 Visual Studio 调试器中这是可能的,但需要学习一些不寻常的菜单命令。 这是您在 Visual Studio 中观察到的趋势吗?还是直接从命令行运行代码时可以看到的线程(例如在ProcessExplorer 中)? @Christophe:您是否建议 Visual Studio 调试器将线程池注入被测进程,但仅限于 Windows 10? 【参考方案1】:

Crystal ball 表示“调试”>“窗口”>“线程”窗口在 ntdll.dll!TppWorkerThread 处显示这些线程。请务必启用 Microsoft 符号服务器自己查看此内容,使用工具 > 选项 > 调试 > 符号。

这也发生在 VS2013 中,所以它绝对不是由新的 VS2015 诊断功能引起的,@Adam 的猜测不可能正确。

TppWorkerThread() 是线程池线程的入口点。当我在此函数上使用 Debug > New Breakpoint > Function Breakpoint 设置断点时。当第二个线程池线程开始执行时,我很幸运地为第一个线程池线程捕获了这个堆栈跟踪:

    ntdll.dll!_NtOpenFile@24()  Unknown
    ntdll.dll!LdrpMapDllNtFileName()    Unknown
    ntdll.dll!LdrpMapDllSearchPath()    Unknown
    ntdll.dll!LdrpProcessWork() Unknown
    ntdll.dll!_LdrpWorkCallback@12()    Unknown
    ntdll.dll!TppWorkpExecuteCallback() Unknown
    ntdll.dll!TppWorkerThread() Unknown
    kernel32.dll!@BaseThreadInitThunk@12()  Unknown
    ntdll.dll!__RtlUserThreadStart()    Unknown
>   ntdll.dll!__RtlUserThreadStart@8()  Unknown

显然,加载程序正在使用 Windows 10 上的线程池来加载 DLL。这当然是新的:)此时主线程也在加载器中执行,并发在工作。

因此,Windows 10 正在利用多核来更快地初始化进程。非常重要的功能,而不是错误:)

【讨论】:

我的问题是:假设我已经分析了我的服务器并且我看到 5 个线程带来了最高的性能。新版本的 Windows 是否可以随意使用我已经创建的线程中的更多线程?我不是要求在 Web 服务器上使用线程池,而是您在这里提出的问题,即“我的程序中不需要的 os 线程” 请注意,Windows 有权创建任意数量的线程。如果你的程序依赖于没有不是你自己创建的线程,那就是一个错误。 @Voo:使用本机代码的一大好处是无需为不使用的资源付费。如果我的应用程序不需要线程池,那么它仍然需要为四个线程的堆栈空间付费似乎很奇怪。如果我使用的是具有大型运行时系统的框架,我不会感到惊讶。但即使是最简单的程序现在也会启动多个线程,即使它从不使用它们。 这是一个BUG!所有用户创建的线程和 main 上的 ExitThread() 不再导致进程终止;这是合同行为。 后来写了一篇文章引用这个答案,详细描述了加载器线程池:threatvector.cylance.com/en_us/home/…【参考方案2】:

这是默认的线程池。 https://docs.microsoft.com/en-us/windows/desktop/procthread/thread-pools

每个进程都有一个默认线程池。

【讨论】:

当然可以,但是,在 Windows 10 之前,如果程序不使用线程池,则不会创建额外的线程。从 Windows 10 开始,所有程序现在都需要支付启动多个线程(计算和内存)的费用,即使它们不需要它们。 如果您确实需要,您可以禁用此功能,请参阅 cmets 中的 Suma 链接到已接受的答案。但是成本是最低的(前 30 秒有几个额外的线程处于等待状态),所以我怀疑在大多数情况下更快的加载时间足以弥补这一点。【参考方案3】:

这也引起了我的注意,所以我决定找到我个人的答案;正如另一张海报所说,它有点像“水晶球”,但是......

可能的原因是您的线程之一:

WaitForSingleObject 或 WaitForMultipleObjects

在最新版本的 Windows 中实现这一点似乎产生了一个线程池以方便等待对象(不知道为什么)。

这也可能发生在你的 main 之前,因为你有一些代码会导致创建一个全局范围的对象,然后在你到达入口点之前启动代码(这甚至可能在 Windows 的一些标准库代码中10 SDK)。

对于任何想找出自己的具体原因的人,您可以试试这个:

class RunBeforeMain

public:
    RunBeforeMain()
    
        HMODULE hNtDll = (HMODULE)LoadLibrary(_T("ntdll.dll"));
        FARPROC lpNeeded = GetProcAddress(hNtDll,"NtWaitForMultipleObjects");
        DebugBreakPoint();
    
;

RunBeforeMain go;

int CALLBACK WinMain(
  _In_ HINSTANCE hInstance,
  _In_ HINSTANCE hPrevInstance,
  _In_ LPSTR     lpCmdLine,
  _In_ int       nCmdShow
)


当你运行它时,你会在 lpNeeded 中获得 NtDll 过程 NtW​​aitForMultipleObjects 的库加载位置,获取该地址并将其粘贴到反汇编视图窗口中,然后在第一行放置一个断点。

现在继续运行您的解决方案。

几个注意事项:

    我们无法有效地控制全局变量的初始化顺序,这就是为什么如果你有良好的编码意识,你会不惜一切代价避免它们(除非有一些特殊的需要)。由于这个事实,我们不能保证我们的全局将在任何其他全局导致额外线程之前触发。 虽然这是在 main 之前,但任何库的 DLL 加载都将继续我们的任何调用,因此,可能已经太晚了(您可以使用诸如强制不自动加载库之类的技巧,但这超出了我的意愿水平在这里照顾大声笑)。

希望这可以帮助某人:)

【讨论】:

以上是关于为啥 Windows 10 在我的程序中启动额外线程?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 SQLite 替换功能会在我的 Android 应用程序中产生错误?

为啥我的 Cordova WebView 有 20px 的额外滚动?

为啥 MIDI 音序器无法在 Windows 10 上第二次播放

iOS 7 addSubview 为我的 UIView 添加了额外的长度,这是为啥呢?

为啥 FileSearch 元素的 MinVersion 属性在我的 WiX 安装程序中不起作用?

为啥 MySQL 不会在我的 XAMPP 安装上启动?