As a temporary solution to allow existing debuggers work with virtual threads, the JDWP agent will track virtual threads so they can be enumerated for debuggers that want to enumerate all virtual threads. The options that control this behavior are:
|Option Name and Value||Description||Default|
|enumeratevthreads=y|n||thread lists include vthreads||n|
|notifyvthreads=y|n||send THREAD_START/END events for all vthreads||y|
- enumeratevthreads control whether or not virtual threads are included in the list of threads returned by JDWP VirtualThread.GetAllThreads and ThreadGroupReference.Children commands. This flag can be turned on for compatibility purposes. This flag will eventually go away and the debug agent will operate as if it was set to 'n'.
- notifyvthreads controls whether or not THREAD_START and THREAD_END events are sent for vthreads. The default is to send them. The purpose is to allow debugger writers to set to 'n' to see how the debugger works without getting these events. This is meant as a short term convenience flag rather than having to set the PlatformThreadsOnly filter as described above. In the long run if debuggers don't want these events, they will need to set the PlatformThreadsOnly filter. Since this means no END event will be sent, the debugger will need to figure out how to "forget" about virtual Threads that have terminated. This could possibly be done with an ObjectReference.IsCollected command. Please note that notifyvthreads=y will not override any PlatformThreadsOnly filter that is in place.
Issues with too many virtual threads hitting breakpoints
Each running virtual thread is mounted on a carrier thread. There are a limited number of carrier threads (usually defaulting to the number of available cores). When a virtual thread hits a debugger breakpoint, it continues to "occupy" its carrier thread, preventing any other virtual thread from running on it. If you have an application with a large number of virtual threads, and you setup a breakpoint that many virtual threads will hit, you can wind up in a situation where every carrier thread is running a virtual thread that is at a breakpoint. No virtual threads will make progress when this happens. Those that are mounted are all at breakpoints, and those that unmounted have no carrier thread to run on. If you resume one of the virtual threads, when it yields it will allow unmounted virtual thread to run, which likely itself will then hit this same breakpoint, leaving back to having all virtual threads being stuck.
If you single step in one of these virtual threads that are at a breakpoint, single stepping will work until you step over a call that can lead to the virtual thread yielding. At this point the virtual thread is umounted and a new one can run on the carrier thread (once again, likely quickly hitting the same breakpoint). It will appear that the single step has failed to ever complete. In reality it is waiting for the yielding virtual thread to be remounted and start running again. This can't happen until a carrier thread is freed up. Thus you'll find that you need to start resuming various virtual threads that are at breakpoints until eventually one of them yields AND its carrier thread is used to mount and run the single stepping virtual thread. There's no telling how many virtual threads at breakpoints you will need to resume before this happens.
It is possible to avoid getting into this situation. The first thing to do is to setup the breakpoint so that it suspends all threads rather than just the event thread. This way you'll never have more than one virtual thread hitting the breakpoint at the same time. If you do this, you also need to make sure to setup the debugger's single stepping "thread resumption policy" to resume all threads, rather than just the single stepping thread. If you take these two steps, breakpoints and single stepping should work much better when dealing with a large number of virtual threads that can hit the same breakpoint.