Here is Process Explorer.
For one thing, there's a column for Threads just like we have in Task Manager.
So it's definitely something we might want to look at.
Under Process Performance, you find Threads, and there's
some other CPU‑related information here.
So you can see the number of threads in every
process. Let me just bring that here.
So this is no different essentially from what we've seen in Task Manager.
However,
the more interesting part in Process Explorer is when you double‑click a
process of interest, and there is a Threads tab here. And the Threads tab
can show us all the threads within the process with some level of detail
that we don't have in Task Manager.
So let me briefly describe the details that we see here
in this view in Process Explorer.
So first, we have some columns here indicating the state
and some properties of the thread.
First, we have the thread ID.
The thread ID is a way to identify a thread throughout the
system as long as that thread object is alive,
just like a process ID.
In fact, thread IDs and process IDs are generated from the same number pool,
so to speak, so there is no way there will be a thread ID and processes ID
that might have the same number while both of them are alive.
They are generated from the same number pool.
That's the way to identify a thread.
Then we have a CPU time here,
which is about how much CPU is consumed by the thead,
and this is updated every second,
just like other views in Process Explorer by
default. We can see the Cycles Delta,
which is the number of CPU cycles that have been used
since the previous update a second ago.
There's the Suspend Count here, which indicates if the thread is
suspended, and if so, what's the suspend count?
You might be wondering, what's the point of having suspend count?
I mean, shouldn't there be just a brilliant value here, either suspended or not?
So not exactly. When the threat is not suspended, then we see nothing here.
However,
if you suspend the thread by calling an API such as suspend thread, the suspend
count is incremented. If other components calls SuspendThread on the same
thread, that suspend count will continue to increment, so you actually have to
call ResumeThread the same number of times to actually make sure that the
particular thread is resuming execution.
So here's an example with something like a UWP process like calculator.
So if you look at the calculator and minimize it, we've
seen these things perhaps before.
Here's calculator,
the process, and we double‑click that, and you'll
notice that the Suspend Count is 1.
So now all the threads are suspended because we know that this kind of
behavior is typical for UWP processes. If I resume the calculator
application, notice the Suspend Count has changed to 0.
So now all these threads are free to run.
They're not necessarily going to run.
That depends on the behavior of the calculator, but at least they have that
ability because the threads are no longer suspended. Again,
if I minimize,
they all become suspended, and there's a bug in Process
Explorer that doesn't update these things very quickly, but
it is updated properly, eventually.
So going back to an example for Explorer here because it has so many threads,
we can sort by thread ID or by CPU or by anything else.
The last column here is the Start Address, and the start address is the one that
indicates what's the function that is used to run this particular thread? We
mentioned at the beginning that a thread is really just a function executing
code that is independent of other functions executing the same or different
code. So obviously, there's some function here that starts this execution, and
this is what we see here.
And if we have proper symbols,
we can see a fairly reasonable name for the symbol for that function.
So, for example, the thread I'm focusing on right now is the one that is
starting Explorer, so that's the main thread for Explorer.
It's running WinMainCRTStartup,
which is the C runtime startup function that eventually
calls the actual WinMain of Explorer.
However, if we go ahead and double‑click this particular thread,
we get the call stack for the thread.
There's also a Stack button at the bottom, which does exactly the same thing.
So in this case, Process Explorer tries to build the
call stack for that particular thread.
Sometimes, it might take a few seconds if Process Explorer is
forced to load symbols from the Microsoft symbol server if it
doesn't have them already cached.
So you can see here, the call stack from the bottom to the top.
And the interesting thing here is that the call stack, if you
are in Process Explorer with admin privileges, encapsulates
the user mode and kernel mode.
In this case,
Process Explorer is using its kernel driver to examine the
call stack on the kernel side of things.
But you'll notice that the first function that's actually being called
is not that function that we've seen here or something that is in
Explorer is something, but, in fact, it's a different function called
RtlUserThreadStart, and that's because that's the function that is
always the first one to run in user mode from ntddll, and so Process
Explorer decides to skip this and the second function because these are
always the same.
It goes to the third one, which is the actual user provided,
so to speak,
function for that particular thread. Later on, this function
calls this one, it calls this one, and so on.
And at some point, we can see that from frame 16 and 15, in this case,
we are transitioning into the kernel. So win32u.dll is still in user
mode, but then ntoskrnl is the actual kernel. So these frames are, in
fact, in the kernel, and this is where the thread is right now, at least
it was at that point when the stack was captured. So you can grab any
one that you want,
any thread you want and see the call stack for the thread, which
might be interesting in some cases. And again,
you'll find that the first two functions are always the same,
but then you have the real function.
So, for example,
one of the common ones you'll see is TppWorkerThread from
ntdll, which means this is a thread pool thread,
which means the thread that is running some kind of work that is handed
over to it or waiting for work to be available. In this particular
example, NtWaitForWorkViaWorkerFactory is, in fact, the function that
waits for work to be available for this thread.
WorkerFactory is the internal name in the kernel given to the
thread pool object that is managed by the kernel.
So that's the system call on the ntdll side, and this is the system
call actually being implemented on the kernel side.
If you are not familiar with system calls,
I recommend you check these things out in the original Windows 11 Internals:
Foundations course, the first one in this series of courses.
So with Process Explorer,
we see details about threads. And if you look at the bottom,
there are some more details here. Let me briefly describe those.
So we have the start time of the thread. We have the state of the thread.
This is one of those values like wait,
ready or running, and wait here is, of course, going to be the
most common. And if we are looking at the wait state, then
there's something here called wait reason, which gives us a hint
as to why the thread is waiting.
This is not something that is guaranteed to be accurate, but this is
something that every function in the kernel that performs a wait
operation can indicate to whoever is interested.
However, waiting is just waiting, regardless of the actual reason.
Then we have kernel time and user time being used by the thread and
other threads that execute system calls, which is practically all
threads that have user time and kernel time.
Again, I refer to you to the first calls in this
series to see what that looks like.
We'll have a number of contexts, which this thread has incurred since its
inception, the number of CPU cycles it has used for actually running code since
it started, and we have some priority details here.
Notice we have two values here. There's a Base Priority and a Dynamic
Priority, and their priority might be slightly higher than the base
priority, and this is temporary, and we'll discuss why these boosts
occur later on in this course.
In any case, the actual priority that matters for the kernel's
scheduler perspective is the Dynamic Priority. And then we have some
other stuff like I/O Priority, Memory Priority,
and Ideal Processor, some of which we'll discuss later in this course.