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.

 

  Native Managed
Attach

 

For Native-debugging, there is a "loader breakpoint", which is a break point event that comes after the load events and serves as an 'attach complete' event for native. This event comes in the normal launch sequence after the load events, and since attach fakes up the same events as launch, it makes sense to come in the attach case too. Managed-debugging has no attach-complete event (MDbg fakes one up to be nice), and so it doesn't have a clear thread to pick.  I would consider this a minor design flaw with managed debugging because you have no way of reliably knowing when the fake events are done and the real events start. (Mdbg uses a heuristic). At the same time, the native loader-breakpoint isn't super-reliable either, but it's better than nothing.
Async-break An Async Break in native-debugging is done by injecting a tiny thread into the debuggee that just generates an break point event (int3 on x86).  (See kernel32!DebugBreakProcess). This is why the "active thread' in most native debuggers would be this random injected thread. VS tries to be nice and hide this thread. Windbg doesn't have any such sugar and will show you this "async break" thread. For manager-debugging, Async-break is done cooperatively by the CLR using similar logic that the GC uses for suspension. For a GC, the thread requesting the GC (usually the thread allocating) pumps the suspension. In V2, our helper-thread pumps the suspension for the debugger.
So when the debugger suspension is complete for async-break, there is no managed thread directly responsible (see ICDThread trivia and so no good candidate for active thread in the managed debugger.

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.