Don't assume that if you have a thread doing a spin-wait, that you can attach / asynchronously-break ("async-break") and your debugger will immediately stop at the spin-wait.
When you attach to a debuggee or async-break while debugging, the debuggee's threads could be anywhere. Even if you attach in response to a debuggee request (such as an unhandled exception or Debugger.Launch), other threads may be anywhere.
Furthermore, there is no notion of "active thread" in the underlying debug APIs (both ICorDebug and windows). It's purely a construct in a debugger UI to make it easier for end-users. Debugger's generally track an active thread that defaults to the last thread sending a debug-event, and the user can also explicitly set it, perhaps via a Threads-list window. Now the MDbg APIs do have a notion of active-thread. It's simply extra state in the MDbg wrappers to let extensions get at that piece of the UI.
Attach vs. Async-Break: In terms of stop-states, Stopping on Attach is very similar to Async-Break in that they both stop the process asynchronously using similar mechanisms. From this perspective, you can view Attach as establishing the debugger connection and then doing an Async-break. So I'll talk about them both together here. The following table looks at both attach and async-break across both managed and native debugging.
Attaching to a known state:We have a lot of debugger tests where we want to attach to a known state with a particular "active thread" in the debugger. The way we do this in our tests is to have the debuggee do something like:
while(!Debugger.IsAttached) { ; }; // loops until a debugger is attached Debugger.Break(); // sets the active thread.
And then the debugger can Attach and let the debuggee run to the Debugger.Break(). Then we get a debug event from a known thread and that becomes the active thread.