VS 2008 中的多线程 C++ 调试窗口






3.85/5 (7投票s)
在 10 分钟内,了解如何使用 VC++ 2008(Orcas)调试窗口的新功能来调试一个示例文 spinner C++ 多线程应用程序

引言
本文将使用一个 MFC/C++ 示例应用程序来演示新的 Visual Studio 2008 调试器,特别是线程调试的功能。
背景
本文介绍了一个 Visual C++ 2008 Express Edition 中附带的新线程调试窗口的演示。读者应具备 Windows 平台线程的足够知识,以及 C++ 和 MFC 的工作知识。
读者应下载一份 Visual C++ 2008 Express Edition。此外,用户计算机上必须存在 C:\Program Files\Microsoft Visual Studio 8\VC\atlmfc 文件夹。我通过使用我机器上的 Visual Studio 2005 的 atlmfc 文件夹,并将 atlmfc 文件夹复制到 Visual C++ 2008 Express Edition 的相应位置来实现这一点,因为免费下载不附带 atlmfc 文件夹。此技术对于本文的目的应该是完全可以的。Visual Studio 2008 Express Edition(Beta)的下载可在 此处获得。
Using the Code
要使用本教程,请启动应用程序并勾选显示的复选框。主视图(CTaskingView
)派生自 CFormView
。它是一个 SDI 应用程序,因此除了标准的 MFC 派生类 App、Doc 和 View 之外,该应用程序还有一个自己独特的类,名为 CSpinner
,它派生自 CObject
。CSpinner
负责在屏幕上绘制“spinner”对象,这些对象位于复选框旁边。每个“spinner”都有一组它循环显示的颜色,代码大量使用了 CDC 类的设备上下文绘图。整个应用程序的重点不在于 CSpinner
类,而是使用每个 spinner 来说明在响应 GUI 输入时执行的某些处理。两个“spinner”用于 OnIdle
处理,另外两个用于在工作线程上执行。
为了了解绘图代码的内部情况,我们可以检查 CSpinner
类的 Draw()
函数
void CSpinner::Draw()
{
// Get a pointer to the device context
CDC *pDC = m_pViewWnd->GetDC();
// Set the mapping mode
pDC->SetMapMode (MM_LOENGLISH);
// Copy the spinner center
CPoint org = m_pCenter;
CPoint pStartPoint;
// Set the starting point
pStartPoint.x = (m_iRadius / 2);
pStartPoint.y = (m_iRadius / 2);
// Set the origination point
org.x = m_pCenter.x + (m_iRadius / 2);
org.y = m_pCenter.y + m_iRadius;
// Set the viewport origination point
pDC->SetViewportOrg(org.x, org.y);
CPoint pEndPoint;
// Calculate the angle of the next line
double nRadians = (double) (m_nMinute * 6) * 0.017453292;
// Set the end point of the line
pEndPoint.x = (int) (m_iRadius * sin(nRadians));
pEndPoint.y = (int) (m_iRadius * cos(nRadians));
// Create the pen to use
CPen pen(PS_SOLID, 0, m_crColors[m_crColor]);
// Select the pen for use
CPen* pOldPen = pDC->SelectObject(&pen);
// Move to the starting point
pDC->MoveTo (pEndPoint);
// Draw the line to the end point
pDC->LineTo (pStartPoint);
// Reselect the previous pen
pDC->SelectObject(&pOldPen);
// Release the device context
m_pViewWnd->ReleaseDC(pDC);
// Increment the minute
if (++m_nMinute == 60)
{
// If the minutes have gone full circle, reset to 0
m_nMinute = 0;
// Increment the color
if (++m_crColor == 8)
// If we've gone through all colors, start again
m_crColor = 0;
}
}
请密切注意,上面的代码并不通过循环来在屏幕上绘制线条。它实际上一次只“绘制”一条线。为了使绘图能够重复发生,我们在线程函数中安装了一个无限循环。通过在应用程序执行期间控制线程的挂起和恢复,我们可以“摆脱”这种看似糟糕的编码。
UINT CTaskingDoc::ThreadFunc(LPVOID pParam)
{
// Convert the argument to a pointer to the
// spinner for this thread
CSpinner* lpSpin = (CSpinner*)pParam;
while (TRUE)
lpSpin->Draw(); // Spin the spinner
return 0;
}
另外,如果您查看 CTaskingDoc
中上述函数的声明,您会发现它被列为一个“static
”成员。这是必需的,因为传递给 AfxBeginThread
的函数必须是“UINT Controlling_function_name(void parameter)
”的形式。
同样重要的是要注意,在我们的应用程序中,任何时候实际上只创建了两个工作线程。它们在对话框的右侧显示为“Thread 1”和“Thread 2”。我们应用程序左侧的另外两个复选框演示了 OnIdle
处理!当我们稍后在本文中讨论线程调试窗口时,包含 OnIdle
调用就变得显而易见了。现在,只需注意,当我们勾选“OnIdle
call”复选框时,CSpinner::Draw()
函数的调用方式有所不同,如下所示
void CTaskingDoc::DoSpin(int nIndex)
{
// Spin the Spinner
m_cSpin[nIndex].Draw();
}
OnIdle
反复调用 CTaskingDoc::DoSpin
,只要有“空闲时间”就会触发,因此得名。每当应用程序的消息队列为空时,OnIdle
就会触发。这就是为什么在单击屏幕时,OnIdle
调用会暂时停止,直到用户完成为止。我们应该注意,应用程序的消息队列与我们运行的两个工作线程是独立的。
调试线程
现在您对代码的工作方式有了一些了解,让我们转向如何使用线程调试器来查看这个示例代码中一些有趣的内容。让我们先参考我们代码的最后一部分
void CTaskingDoc::SuspendSpinner(int nIndex, BOOL bSuspend)
{
// if suspending the thread
if (!bSuspend)
{
// Is the pointer for the thread valid?
if (m_pSpinThread[nIndex])
// Suspend the thread
m_pSpinThread[nIndex]->SuspendThread();
}
else // We are running the thread
{
// Is the pointer for the thread valid?
if (m_pSpinThread[nIndex])
{
// Resume the thread
m_pSpinThread[nIndex]->ResumeThread();
}
else
{
int iSpnr;
int iPriority;
// Which spinner to use?
switch (nIndex)
{
case 0:
iSpnr = 1;
iPriority = THREAD_PRIORITY_NORMAL;
break;
case 1:
iSpnr = 3;
iPriority = THREAD_PRIORITY_LOWEST;
break;
}
// Start the thread, passing a pointer to the spinner
m_pSpinThread[nIndex] =
AfxBeginThread(ThreadFunc, (LPVOID)&m_cSpin[iSpnr],
iPriority);
}
}
}
这里最值得关注的是函数末尾的 AfxBeginThread
调用。工作线程就是使用此函数创建的,同时我们还可以设置新工作线程的优先级。在我们的示例中,我们比较了“Normal”和“Lowest”的优先级。我们在 ::SuspendSpinner
调用开始处设置了一个断点,以演示我们对线程调试器的使用。
现在以调试模式启动应用程序。单击应用程序中标记为“Thread 1”的复选框。您应该会命中我们的断点。现在,剩下要做的就是从 IDE 主菜单中选择 Debug->Windows->Threads。您将看到一个窗口出现,类似于下图

请注意此窗口中的一些事项。ID 和 Name 列唯一标识了线程。在这种情况下,我们处理的是主应用程序线程,并且在我们的程序中尚未创建其他线程!这是有道理的,因为 AfxBeginThread
尚未被调用,这使我们进入下一步。您可以将第二个断点设置在 AfxBeginThread
上,然后按 F5 使程序执行稍微向前推进。这里有一个小提示。我喜欢使用键盘快捷键,对于 Visual C++ 2008 Express,它们与 Visual Studio 6.0 相同(在 Visual Studio 2005 环境中,它们略有不同)。无论如何,如果您按一次 F10(单步跳过),请观察线程调试窗口中的新条目!

现在,这里有一些需要注意的地方。主线程的颜色始终是绿色,工作线程始终是黄色。另请注意,这两个线程的优先级都设置为“Normal”,并且这两个线程都没有被挂起(Suspend == 0
)。还有一个很方便的小功能,您可以尝试一下:将光标放在 Name 列上,然后右键单击。有一个选项可以将线程重命名为有意义的名称(例如 Thread1
)。如果我们调试一个使用许多线程的应用程序,这会非常有用。
按 F5 继续执行我们的程序,但这次选择“Thread 2”,并密切注意当我们单步跳过 AfxBeginThread
调用时会发生什么。您可能猜到了,我们的第二个工作线程会出现,这次的优先级设置为“Lowest”。如果您观察传递给 AfxBeginThread
的参数,这与线程创建的方式一致。您应该看到一个类似于下图的窗口

我们将 OnIdle
处理包含在本篇文章中,只是为了说明实际上并没有新的线程因其调用而在应用程序中生成。您可以在 OnIdle
调用中设置断点,观察线程调试窗口,并注意到没有创建额外的线程。
现在我们已经创建了这两个工作线程,并且它们正在我们的应用程序中运行,您可以单击其中一个或两个复选框,并观察线程调试窗口中的 Suspend 列被设置为“1
”。

关于线程调试窗口,还有一个“技巧”。在调试窗口的任何时候,您都可以将光标放在执行指针上,并观察调用堆栈!请记住,所有线程都有自己的执行堆栈,包括主执行线程。相当酷,是吧?好了,您已经拥有了开始使用 Visual C++ 2008 中的线程调试器所需的一切,它将在最终发布时提供,我听说是在二月!
参考
此代码改编自 SAMS Teach Yourself Visual C++ 6 in 21 Days -- David Chapman with Jeff Heaton 的原始版本。
历史
- 版本 1.1.0 (主版本, 次版本, 修订版本)