41
votes

With Visual Studio 2015, in a new, empty C++ project, build the following for Console application:

int main() {
    return 0;
}

Set a break point on the return and launch the program in the debugger. On Windows 7, as of the break point, this program has only one thread. But on Windows 10, it has five(!) threads: the main thread and four "worker threads" waiting on a synchronization object.

Who's starting up the thread pool (or how do I find out)?

3
Here comes the joke about Windows and endless loops...Alexander Shishenko
Maybe processes get a thread pool by default on Windows 10.Jonathan Potter
I'd start by putting a breakpoint on CreateThread. Note that placing breakpoints by name is very common using windbg, while in the Visual Studio debugger it's possible but requires learning some unusual menu commands.Ben Voigt
Is it the trhads you observe within Visual Studio ? Or it it the threads that you can see (for example in ProcessExplorer) when you run your code directly from the command line ?Christophe
@Christophe: Are you suggesting that the Visual Studio debugger is injecting a threadpool into the process under test, but only on Windows 10?Adrian McCarthy

3 Answers

33
votes

Crystal ball says that the Debug > Windows > Threads window shows these threads at ntdll.dll!TppWorkerThread. Be sure to enable the Microsoft Symbol Server to see this yourself, use Tools > Options > Debugging > Symbols.

This also happens in VS2013 so it is most definitely not caused by the new VS2015 diagnostic features, @Adam's guess cannot be correct.

TppWorkerThread() is the entrypoint for a thread-pool thread. When I set a breakpoint with Debug > New Breakpoint > Function Breakpoint on this function. I got lucky to capture this stack trace for the 1st threadpool thread when the 2nd threadpool thread started executing:

    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

Clearly the loader is using the threadpool on Windows 10 to load DLLs. That's certainly new :) At this point the main thread is also executing in the loader, concurrency at work.

So Windows 10 is taking advantage of multiple cores to get the process initialized faster. Very much a feature, not a bug :)

3
votes

It's the default thread pool. https://docs.microsoft.com/en-us/windows/desktop/procthread/thread-pools

Every process has a default thread pool.

0
votes

This intreeged me also, so I decided to find my personal answer; As another poster says, its a bit of a "crystal ball" endevour, but...

The probable cause is one of your threads called either:

  • WaitForSingleObject or
  • WaitForMultipleObjects

The implementation of this in the latest versions of Windows seems to spawn a thread pool to facilitate waiting for objects (don't know why).

This might also possibly be happening before your main because you have some code which causes a global scoped object to be created which then starts off code before you even hit your entry point (this may even be in some standard library code for Windows 10 SDK).

For anyone wanting to find out their own SPECIFIC cause, you can TRY this:

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
)
{
}

When you run this, you will get the library load location for NtDll procedure NtWaitForMultipleObjects in lpNeeded, grab that address and paste it into the disassembly view window then place a breakpoint on the first line.

Now continue running your solution.

Couple of caveats:

  1. We can't effectively control the initialisation order of globals, this is why if you've got good sense coding you avoid them at all costs (unless theres some exeptional need). Due to this fact, we can't guarentee our global will trigger before whatever other global causes additional threads.
  2. Whilst this is before main, the DLL loads of any libraries will proceed any of our calls, therefore, it might be already too late (you can use hacks like forcing no auto loading of libraries but that's way beyond my level of willingness to care here lol).

Hope this helps someone :)