Understanding Processor Affinity and the Scheduler: The Implementation Details
|
|
This article is a technical exposé of how I implemented the "Thread Affinity Explorer" described in a companion article. There is no particular order to the techniques I describe here, but in writing it, I realized that I had brought together a massive number of techniques that I'd developed or used in the past to make this all work, so showing how they all fit together in a single modest application would be an interesting discussion.
When you use ::CreateProcess to launch a process, you get to specify a number of parameters in the STARTUPINFO structure. Among these are the x,y coordinates of where the window should be placed.
I discovered that when the program launched a child process which was a nominal copy of itself, it appeared directly on top of the copy that was already running. I thought this produced a bit of logical disruption to the user, who merely seemed to see the program reappear but with some controls missing. So, I decided, I'll use the STARTUPINFO parameters dwX and dwY to specify the target location of the window.
It is worth pointing out here, for those who think Hungarian Notation ever made sense, that the position is specified in screen coordinates. The type is declared as DWORD type, which is an unsigned integer type. Of course, in Windows, with multiple monitors, screen coordinates can be negative in both X and Y directions. Why is this a DWORD? Historical accident, obviously; someone doing the design did not anticipate multiple monitors. So why not change it to an int? Because the name embodies the type, and the name is dw! In the real world of programming, it should be possible to change the type of a value without changing its name. This would make sense. Hungarian Notation should never be used to represent physical data types. This is not even how it was designed by Charles Simonyi, but the Windows group adopted his ideas without thinking them through and the result is this kind of insanity.
So, I set the dwX and dwY parameters. The window came up in the same place as before! I finally realized that the reason was that a dialog-based application centers itself in the main screen (monitor 1), and therefore it ignores the positioning information passed into it!
The solution was to use the little-known ::GetStartupInfo API call, which retrieves the STARTUPINFO block. I do this in the OnInitDialog handler. I check to see if the STARTF_USEPOSITION flag is set, and if it is, I reposition the window according to the values set by the parent.
STARTUPINFO startup; ::GetStartupInfo(&startup); if(startup.dwFlags & STARTF_USEPOSITION) { /* a position had been set */ SetWindowPos(NULL, startup.dwX, startup.dwY, 0,0, SWP_NOSIZE | SWP_NOZORDER); } /* a position had been set */
It turns out this almost always worked. But what I'd done, to keep the windows from falling off the bottom or right end of the screen, was to reset the position to (0,0) under such conditions. But when I created the first "wraparound" window, it was created in dead-center. Apparently, the dialog code has the property that if you return from OnInitDialog with the window positioned at (0,0), this is an indication that it should be centered. This is undocumented. However, when I changed the wraparound to position to (1,1) it worked correctly!
Many readers may have already done SDI or MDI apps with splitter windows. But the ability to do a splitter window in a dialog appears to be tricky. The good news is: it isn't.
The basic idea is to have an OnSetCursor handler that recognizes when you're in a "splitter" region and sets the correct cursor. For an application like this, it was easy; there was only one splitter bar, the vertical region between two particular windows. So all I had to do was to check to see if the cursor was > the right of the left window, < the left of the right window, and > the top of the left window. This simple geometric test is done by the following code
/**************************************************************************** * CaffinityDlg::InSplitterArea * Inputs: * CPoint cur: Point where cursor is found, in client coordinates * Result: BOOL * TRUE if the mouse is in the splitter area * FALSE if the mouse is outside the splitter area * Notes: * +--------------------------------------------------------+ * | <A> | * | | * | | * |L G | * |+-----------------------------------+##+---------------+| * || c_Log <B> |##| graphs[0] <C> || * |+-----------------------------------+##+---------------+| * +--------------------------------------------------------+ ****************************************************************************/ BOOL CaffinityDlg::InSplitterArea(CPoint cur) { CRect L; c_Log.GetWindowRect(&L); ScreenToClient(&L); if(cur.y < L.top) return FALSE; // in area <A> if(cur.x < L.right) return FALSE; // in area <B> if(graphs.GetSize() == 0) return FALSE; CRect G; graphs[0]->GetWindowRect(&G); ScreenToClient(&G); if(cur.x > G.left) return FALSE; // in area <C> return TRUE; // none of the above, must be in splitter bar } // CaffinityDlg::InSplitterArea
This function is then called from the OnSetCursor handler.
/**************************************************************************** * CaffinityDlg::TrySetCursor * Inputs: * UINT nHitTest: * Result: BOOL * TRUE if we set the cursor * FALSE if we didn't * Effect: * Sets the drag cursor if we are in the drag area ****************************************************************************/ BOOL CaffinityDlg::TrySetCursor(UINT nHitTest) { if(nHitTest != HTCLIENT) return FALSE; CPoint cur; ::GetCursorPos(&cur); ScreenToClient(&cur); if(!InSplitterArea(cur)) return FALSE; HCURSOR drag = AfxGetApp()->LoadCursor(IDC_RESIZER); ::SetCursor(drag); return TRUE; } // CaffinityDlg::TrySetCursor /**************************************************************************** * CaffinityDlg::OnSetCursor * Inputs: * CWnd * pWnd: ignored, except to pass to superclass * UINT nHitTest: interesting if HTCLIENT, otherwise passed to superclass * UINT msg: ignored, except to pass to superclass * Result: BOOL * TRUE if cursor has been set * FALSE otherwise * Effect: * Sets the drag cursor if we are in the drag area ****************************************************************************/ BOOL CaffinityDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) { if(TrySetCursor(nHitTest)) return TRUE; return CDialog::OnSetCursor(pWnd, nHitTest, message); }
The function is also called from the OnLButtonDown handler.
I believe in "rule-based" programming. In this model, you filter things out, progressively, until all that is left is what you want. The above code exhibits this. Anything that is FALSE is filtered out, so all that is left is what to do if there is a TRUE. In the above case, all that is required is to return TRUE.
In other situations, you have the problem of having to introduce a goto to handle the cases that require some kind of recovery, particularly if you have to take some default action. So code could be written that, instead of returning FALSE did a goto to the "what to do if FALSE" case. But this kind of code is ugly, and hard to maintain. I prefer to split conditions like this up into multiple functions. Then I can apply the rule-based model to each function, and not have to worry about early exits, gotos, and similar nonsense.
For example, in the OnLButtonDown handler, I want to handle the default case with the superclass handler, but if I'm in the splitter bar area I want to not take the default case. So the OnLButtonDown handler is very simple:
void CaffinityDlg::OnLButtonDown(UINT nFlags, CPoint point) { if(TryLButtonDown(point)) return; CDialog::OnLButtonDown(nFlags, point); }
If any condition detected is the "false" condition, TryLButtonDown will return FALSE, and I'll fall through to the CDialog::OnLButtonDown handler. If the button press was of interest, I'll return immediately, bypassing the remainder of the function. No gotos, no complex nesting, etc. required!
Now, let's look at TryLButtonDown:
BOOL CaffinityDlg::TryLButtonDown(CPoint pt) { if(!InSplitterArea(pt)) return FALSE; CRect L; c_Log.GetWindowRect(&L); ScreenToClient(&L); CRect G; c_Graphs[0]->GetWindowRect(&G); ScreenToClient(&G); SetCapture(); dxl = pt.x - L.right; dxr = G.left - pt.x; return TRUE; } // CaffinityDlg::TryLButtonDown
(This is a little bit simplified from the actual code, because I don't want to introduce extraneous issues into this discussion. On extra test, unrelated to the splitter problem, has been deleted).
First, I check to see if I'm in the splitter area. If I'm not, I just return FALSE because I don't care about any of the rest of the code. Otherwise, I do a mouse-capture, and save the relative-position of the mouse with respect to each edge of the splitter bar,
The OnMouseMove handler will resize the windows if mouse capture is set.
static int MIN_DRAG_SIZE = (3 * ::GetSystemMetrics(SM_CYCAPTION)); void CaffinityDlg::OnMouseMove(UINT nFlags, CPoint point) { if(Primary && GetCapture() != NULL) { /* can drag */ CRect r; GetClientRect(&r); if(point.x < r.left + MIN_DRAG_SIZE) return; if(point.x > r.right - MIN_DRAG_SIZE) return; // We can move it CRect L; c_Log.GetWindowRect(&L); ScreenToClient(&L); c_Log.SetWindowPos(NULL, 0, 0, point.x - dxl, L.Height(), SWP_NOMOVE | SWP_NOZORDER); for(INT_PTR i = 0; i < graphs.GetSize(); i++) { /* move each */ CRect G; graphs[i]->GetWindowRect(&G); ScreenToClient(&G); graphs[i]->SetWindowPos(NULL, point.x + dxr, L.top, r.right - (point.x + dxr), L.Height(), SWP_NOZORDER); graphs[i]->Invalidate(); } /* move each */ ResizeCopyRect(); } /* can drag */ CDialog::OnMouseMove(nFlags, point); }
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft. In fact, I am absolutely certain that most of this essay is not endorsed by Microsoft. But it should have been during the VS.NET design process!