When you hit a managed-breakpoint while managed-only debugging, only the managed threads are stopped. So threads that are not running managed code can continue to run and won’t stop unless they hit managed code. If a thread was in managed code but called out to native code, it will continue to run. It will be stopped if it returns back to managed code.
This has the same property as the CLR’s garbage collector. A GC won’t stop threads that are not running managed code. Since those threads can’t be touching the GC’s heap anyways, there’s no need for the GC to coordinate with them. Suspending the debuggee at a managed breakpoint uses the same suspension logic that the GC uses.
This has some subtle yet significant properties:
1) This can be viewed as accidental and primitive partial-process debugging support. For example, if you’re managed debugging a GUI app with a native render thread, the app will continue to draw even when stopped at a managed breakpoint. However, if you the render thread runs any managed code, then it will stop and the app’s UI won’t refresh.
2) It means managed-debugging is truly limited to just inspecting the managed state.
3) In native-debugging, you can suspend all threads, detach, and then a different debugger could reattach and resume threads. You can’t do this in managed debugging because the native threads are still running in the detached window.
FWIW, If you’re interop-debugging, then all threads will be stopped. Once the managed suspension is complete, all native threads will get hard suspended.