Winforms + Mdbg threading issue
Why build a gui on top of the shell?
The MDbg shell provides a lot of functionality (such as all the shell commands) that a UI could reuse. For example, MDbg already has a step-over command (“next”). If the UI can just hook up F10 to issue the “next” command to the shell, then the UI can avoid having to implement the stepping commands.
The UI can also has tool windows (such as callstack, thread, and module windows) that need information from MDbg. It can get this information in several ways:
1) It could issue a command to the shell (eg, “where” to get the callstack), capture the textual output, parse that, and dump the information to a tool window. This is brittle because the output could easily change and that would break the parsing. Also, the tool window can only get information from the text output.
2) It could use the underlying MDbg object model to get the information directly. In the callstack case, the UI would implement its own “where” command that would dump output to GUI elements instead of printing it to a console.
MDbg’s object model and shell extensibility story let us have our cake and eat it to. We can use shell commands whenever convenient; and use the object model to do rich queries.
Hooking into the shell:
MDbg exposes a IMdbgIO interface (see MdbgExt.cs) which has 2 methods:
void WriteOutput(string outputType,string output);
bool ReadCommand(out string command);
ReadCommand is actually a callback from an MDbg worker thread. It provides commands to the caller via the ‘command’ out parameter.
The UI’s main form also implements this interface. WriteOutput appends the main form’s output text box. ReadCommand gets a command from the main form’s input text box. (See GuiMainForm.cs).
When the gui is loaded, it plugs in its implementation of the IMdbgIO interface to redirect the input and output to the gui.
Navigating the threading minefield:
I strongly believe that threads are evil and should be avoided whenever possible. Multiple threads leads to race conditions and nondeterministic bugs, and that will only cause great pain. Now that said, sometimes you have to use multiple threads.
UI design is one of those times because the UI thread must never block (or the UI will hang) and so it must offload real work to a worker thread.
Here are the only threads we need to worry about here and their key properties:
1) The UI thread.
a. All UI notifications (such as button clicked, menu clicked, etc) come on this thread.
b. This is the only thread that can update any UI elements (such as enabling / disabling controls, updating the window text)
c. This thread must never block, else the UI will hang. Therefore we want this thread to do as little real work as possible.
d. Other threads can post work to the UI thread via the Control.Invoke method.
2) The MDbg Command thread.
a. This is the real worker thread. It can block (on both MDbg commands and on the UI thread).
b. The IMDbgIO.ReadCommand is called on this thread. This thread can block inside that call waiting for some command, and then it returns and lets the MDbg framework execute the command.
c. This thread can’t update any UI state since it’s not the UI thread. It can use Control.Invoke to make blocking calls to update the UI.
The underlying ICorDebug implementation is thread safe. However, the MDbg object model that sits on top of it is not thread safe, although it can be accessed from multiple threads if those threads guarantee coordinate themselves to use it at separate times.
Putting it all together:
We should have all the key background info now. Here’s the way this gui sample handles the problems.
We have several global objects that the threads use to communicate:
- An event (m_inputEvent): This gets set by the UI thread when a command is available for processing. The ReadCommand will wait on this event.
- a text buffer (m_cmdTxt): This is the command string that the UI is sending to the worker thread.
- A flag for whether it’s safe for the UI thread to use the MDbg objects (ActiveProcess). This flag is non-null iff the command thread is blocked waiting for a command. If the command thread is blocked, it can’t be using the MDbg object model, and so it’s safe for the UI thread to use the object model. This flag is only set and read by the UI thread.
The Gui’s impl of ReadCommand will:
1) make a call to the UI thread to notify it that the shell is ready for input. (SetCommandInputState(true)) . This will set ActiveProcess to non-null and also update all the UI windows.
2) Wait for an event (m_inputEvent) to get signaled by the UI thread. This blocks the shell until we get a command.
3) Set the output parameter from the global m_cmdTxt and return. The caller (inside of MDbg) will execute the command and call into ReadCommand when it’s ready for more input.
The UI thread will call ProcessEnteredText(string input) when it wants to send a command to the Command thread. It will:
1) This will notify the UI that the debuggee is about to start running (SetCommandInputState(false)) running and also set ActiveProcess to null (which will tell the rest of the UI to no longer use the MDbg objects).
2) This will set the global buffer (m_cmdTxt) to the input command
3) then set the m_inputEvent to let the ReadCommand function resume executing.
Since ProcessEnteredText is making an asynchronous post to the command thread, the UI thread has no idea what the world looks like after this call. Once the command has finished executing, the command thread will call into ReadCommand again, which will refresh the UI.
Notice throughout all of this that:
- The UI thread never blocks.
- The UI thread and the Command thread guarantee exclusive access the MDbg object model (via the ActiveProcess property).
- The UI thread can directly use the rich MDbg object model to update tool windows when the debuggee is stopped (as opposed to having to send a stream of asynchronous messages between the threads). This makes it much easier to write tool windows on the UI thread.
- The UI thread can also issue commands to the Command thread.
- The command thread never updates the UI directly. It always makes a blocking call to the UI thread (via Control.Invoke to SetCommandInputState).