多线程实用指南 - 第一部分






4.89/5 (121投票s)
从本指南中了解如何以及何时——以及何时不——使用多线程。
引言
本文不介绍多线程、进程、处理器等概念。也不提供线程函数的语法细节。它只展示多线程编程的实用方法。如果您不了解多线程,请阅读网络上的一些文章,了解语法、如何编写控制台应用程序以及如何同时使用两个线程在控制台写入内容。了解一个线程如何先于另一个线程完成,以及类似的内容。在这里,我只关注实用方法。例如,“如何取消或暂停处理?”(这是最简单的问题之一!)。
目标读者
- 了解多线程的基础知识
- 了解 MFC 或 .NET Framework 中的 GUI 编程
本文中的示例代码和图片均以 C++/C# 和 MFC/.NET GUI 的形式呈现。您可以下载所需的项目。
实用示例 1
您正在进行一项耗时的处理,并显示一个进度条以及一些反映处理状态的文本。
下面是执行处理的代码
private void btnProcess_Click(object sender, EventArgs e)
{
int nStart = Convert.ToInt32(txtStart.Text);
int nEnd = Convert.ToInt32(txtEnd.Text);
prgProcess.Minimum=nStart;
prgProcess.Maximum=nEnd;
for (int nValue = nStart; nValue <= nEnd; nValue++)
{
lblStatus.Text = "Processing item: "+ Convert.ToString(nValue);
prgProcess.Value = nValue;
}
}
正如您所见,它似乎运行良好,但不如预期。状态文本没有更新(下载并运行代码)。此外,如果您输入一个很高的值,比如下图所示,然后尝试操作窗口,窗口将无响应!您可能会看到如下内容(取决于您的操作系统)
那么,如何解决这个问题呢?最简单也是最糟糕的解决方案之一是调用 Application.DoEvents
(在 C# 中)。MFC/Windows API 程序员可以调用 PeekMessage
和 DispatchMessage
序列来实现相同的功能,但为简化起见,此处未显示。
for (int nValue = nStart; nValue <= nEnd; nValue++)
{
lblStatus.Text = "Processing item: "+ Convert.ToString(nValue);
prgProcess.Value = nValue;
// Let Windows (application) process user actions (and other messages)
Application.DoEvents();
}
在这段代码中,您可以看到它运行完美!
那么,既然这段代码解决了问题,为什么还需要多线程呢?嗯,有很多问题!
让我们从一个简单的问题开始。您可以再次单击按钮开始处理(甚至更改起始/结束值)。这将根据所选值使进度条向前或向后移动。这也会是随机的!
那又怎样?只需在处理完成之前禁用所有控件,然后重新启用控件!
在这种编程的早期阶段,它通常有效,因此不需要另一个线程。但是,当例程(函数)执行一些高级任务时——例如从文件读取、加密/解密文件内容、验证文件中的单词、通过 Internet 发送它们,或者仅仅是复制到另一个文件——这将不起作用。您可能需要多次调用 DoEvents
(PeekMessage
/DispatchMessage
)。
ReadFile();
DoEvents();
VerifyContents();
DoEvents();
DecryptFile();
DoEvents();
bool bDuplicate = AreWordsDuplicate();
DoEvents();
if(bDuplicate)
{
ProcessDuplicate();
DoEvents();
}
else
{
...
}
...
如上述代码所示,在例程的多个阶段调用 DoEvents
是过度了。这肯定会减慢处理速度。此外,您还需要妥善管理 DoEvents
的调用。如果用户点击了“取消”(假设您保持启用状态)怎么办?那么您需要一个全局的、类级别的变量来检查用户是否已取消,并且必须在每次调用 DoEvents
后进行检查。
有时,您也可能无法禁用控件,而一组控件可能会触发另一个事件。例如,如果您有一个名为“CopyAllFiles
”的长例程,并且您放置了一组平衡良好的 DoEvents
和“HasUserPressedCancel
”调用。但是,如果用户按下“DeleteFile
”、“RenameFile
”或其他类似的按钮怎么办?您绝对无法控制用户操作。此外,您正在限制用户高效地使用您的应用程序。仅仅因为您想避免多线程?
另一个问题!(是的,我知道有些读者对多线程有理由,但我必须为了完整性而说明!)
调用 DoEvents
可能导致堆栈溢出异常!这会崩溃应用程序。您可能想知道为什么会发生这种情况。嗯,DoEvents
搜索窗口的事件(事件可以由用户或系统触发)。如果它找到一个事件(在 Windows 术语中,它是 Windows 消息),它会调用相应的事件/消息处理程序(函数),而该处理程序也可能调用 DoEvents
,而 DoEvents
又可能调用其他函数。这个过程可能会一直持续下去,直到……嘟!异常!您的应用程序崩溃了!
结束。故事结束。不要使用 DoEvents,或者其他语言/框架中的等效变体。
让我们动手实践多线程吧!
您可能非常想看线程是如何创建和管理的。实际的代码?请稍等片刻。以下段落非常重要!请阅读。
Windows 和 Threads 是不同的概念,但目前,请假设它们是相同的。一个 Windows 窗口可以同步或异步地处理消息。
- 同步:当您通知一个窗口处理消息/事件时,并且在消息处理完毕之前不会返回。在 Windows API/MFC 中,您使用
SendMessage
API 来通知窗口(消息接收者)。SendMessage
在窗口处理完消息之前不会返回。在 .NET 中,您可以通过使用Control.Invoke
方法来实现相同的功能。 - 异步:您通知窗口,但不等待查看消息是否由窗口处理。您通过使用
PostMessage
API 来实现相同的功能。PostMessage
API 会将指定的消息放入目标窗口的消息队列(接收者,将处理该消息)。您不管理消息队列,它由 Windows 操作系统管理。在 .NET 中,您可以使用Control.BeginInvoke
方法来实现相同的功能。
管理线程
下表列出了与线程相关的常见操作以及如何使用不同的编程元素来实现它们。此列表不包括在线程运行时与之通信,也不包括线程安全性、线程优先级等。本文不使用 POSIX 线程,但为完整性起见在此列出。
- * C# 编译器会警告我们应该使用其他技术。我们也会这样做,我稍后会介绍!
- ** 这些是非可移植 API,未经 POSIX 官方支持。
- # 强制终止线程不推荐。极少数情况下,我们可能需要这样做。我们稍后会讨论。
- ^
sleep
或usleep
函数会挂起整个进程! - ^^ 使用“0”信号的
pthread_kill
不合适。具有相同 ID 的另一个线程可能已被创建。我们需要使用pthread_key_*
函数集。详细介绍这些内容完全超出了本文的范围。
为了优雅地结束线程:始终建议让线程函数(线程的根函数,就像进程的“main
”函数一样)返回并正确结束线程。调用相应的函数(如列表所示)以“优雅地结束线程”存在一些问题。其中一些可能是:打开的文件可能未关闭,C++ 类对象不会通过析构函数销毁,线程同步对象可能不一致(稍后讨论),以及其他问题。
我假设读者对线程有基本的了解,但我仍然会提到几点
- 线程在进程地址空间中运行。进程至少有一个线程。应用程序启动时创建的第一个线程是主线程。
- 每个线程都有自己的(函数)调用堆栈。即使您创建了多个同名线程(即相同的函数/线程例程),它们也会有不同的调用堆栈。
- 线程可能有几种“状态”,例如运行、等待 I/O、阻塞等。
- 导致未被线程处理的异常(如零除或空指针赋值)的线程会使整个进程崩溃!
好的!这是创建线程的代码。选择的是最简单的方法。
MFC
UINT Example1Thread(void*);
void CExample1_Dlg::OnBnClickedOk()
{
AfxBeginThread(Example1Thread, this);
// OnOK();
}
UINT Example1Thread(void *pParam)
{
CExample1_Dlg* pThis= (CCExample1_Dlg*)pParam;
// Use pThis
// Perform processing
return 0;
}
请注意“Example1Thread
”的语法。AfxBeginThread
需要一个返回“UINT
”并接受“void*
”的函数指针。void*
参数的内容与我们传递给 AfxBeginThread
的第二个参数相同。由于我们知道确切的类型,因此将其进一步转换为 C++ 类类型。必须注意的是,AfxBeginThread
/CreateThread
请求操作系统创建线程并最终运行它。线程创建函数不会等待线程例程完成(否则,多线程就没有意义了!)。
C# / .NET
// using System.Threading;
private void btnStartThreaded_Click(object sender, EventArgs e)
{
Thread MyThread = new Thread(ProcessRoutine);
MyThread.Start();
}
void ProcessRoutine()
{
// Do processing here.
// We better make this method 'static'
// But we'll take those things later.
// Here we can use all member of class directly (since we have 'this')
}
好的!现在我们将代码放入“ProcessRoutine
”,它位于“btnProcess_Click
”(参见上面的第一个代码片段)。当我们运行时(按 F5),Visual Studio 为我们开启了一个新世界!它会中断调试会话,并显示 Cross Thread Access 异常。
如果您在不调试的情况下运行(即使用 Ctrl+F5),程序将运行并按预期工作。但程序随时可能崩溃。MFC 应用程序也会发生类似的情况。调试器可能会显示一些远程位置的断言,显示一些 MFC 库的陌生代码。请记住,MFC 窗口类不是线程安全的。
为什么会发生这种情况?
您知道不能同时以写模式打开同一个文件并更改其内容(如果您知道,请忽略高级方法!)。文件、窗口、套接字、数据缓冲区、屏幕(和其他设备)、数据库表等都是对象。这些对象大多不允许同时修改它们。原因很简单:一致性! 通常,您按照以下模式修改对象:
- 等待对象空闲以便修改
- 打开对象进行修改
- 修改对象的内容
- 关闭对象以便其他人打开它
您可以列出一些允许同时修改的对象类型(如数据库表)。嗯,它们在内部使用多线程概念,您很快就会了解到!
这与当前的问题有什么关系?
窗口(或者说控件)是由某个线程创建的,通常是应用程序的主线程,但这并非必然。该线程拥有修改内容的“写”访问权。我们正在尝试使用另一个线程修改内容,这违反了我们之前讨论的“一致性”规则。我们也无法使用 4 步过程(等待、打开、修改、关闭)来修改窗口(对象)的内容。关闭窗口对象实际上意味着销毁窗口(控件)。请注意,修改窗口对象不仅意味着更改文本/标题,还包括修改其任何属性(如颜色、阴影、字体、大小、方向、可见性等)。
那么,有什么办法可以解决?
我们可以发送或发布消息给窗口!如果您记得清楚,发送消息(通过 SendMessage
/Invoke
)是同步操作。这意味着您会通知一个窗口并等待目标窗口完成操作。例如,您可以从另一个窗口发送 WM_SETTEXT
消息来设置目标窗口的文本。不要对这个奇怪的名称(WM_SETTEXT
)或 SendMessage
的实际用法感到困惑。随着我们继续前进,事情会变得清晰!以这种方式调用 SendMessage
与调用 SetWindowText
(参见代码)相同。
UINT Example1Thread(void *pParam)
{
CExample1_Dlg* pThis= (CExample1_Dlg*)pParam;
pThis->SendMessage(WM_SETTEXT, 0,(LPARAM)L"New Caption!");
// eq. to ::SendMessage(pThis->m_hWnd, WM_SETTEXT, 0,(LPARAM)L"New Caption!");
// pThis->SetWindowText("New Caption!");
// SetWindowText internally calls SendMessage,
// which sends WM_SETTEXT message to target window.
return 0;
}
从任何线程向任何窗口调用 SendMessage
都是安全的!但这不适用于 MFC 对象,也不适用于 .NET 控件类。请注意。它们代表复杂的类对象,而不是简单的句柄。好的,我在这里不详细说明,但会继续我们的多线程会话。:)
继续解决问题……
现在,让我们以正确的多线程方式更新进度条和状态文本。
// Delegates - same as function pointers.
// First line declares type, second declares variable
delegate void DelegateType(int x);
DelegateType TheDelegate;
// We store the Start and End values in class-variable,
// so that 'thread' actually does only processing.
int StartFrom, EndTo;
private void btnStartThreaded_Click(object sender, EventArgs e)
{
// Set the delegate.
TheDelegate = MessageHandler;
StartFrom = Convert.ToInt32(txtStart.Text);
EndTo = Convert.ToInt32(txtEnd.Text);
prgProcess.Minimum = StartFrom;
prgProcess.Maximum = EndTo;
// Disable button, so that user cannot start again.
btnStartThreaded.Enabled = false;
// Setup thread and start!
Thread MyThread = new Thread(ProcessRoutine);
MyThread.Start();
}
// This is delegated function, runs in the primary-thread
// (i.e. the thread that owns the Form!)
void MessageHandler(int nProgress)
{
lblStatus.Text = "Processing item: " + Convert.ToString(nProgress);
prgProcess.Value = nProgress;
}
void ProcessRoutine()
{
for (int nValue = StartFrom; nValue <= EndTo; nValue++)
{
// Only actual delegates be called with Invoke, and not functions!
this.Invoke(this.TheDelegate, nValue);
}
}
我建议您学习委托。简而言之,委托就像 C/C++ 中的函数指针。您将某个委托变量设置为适当类型的函数。然后,您调用 Invoke
或 BeginInvoke
,它最终会在控件所属线程的上下文中调用该函数。请记住,所有控件(窗体、组合框、进度条等)都派生自 System.Control
。在上面的示例中,我禁用了按钮,以免用户再次开始处理。
MFC 方法
UINT Example1Thread(void*);
void CExample1_Dlg::OnBnClickedOk()
{
// Get start and end values
StartFrom = GetDlgItemInt(IDC_START);
EndTo = GetDlgItemInt(IDC_END);
// Set progress bar range
ProgressBar.SetRange(StartFrom, EndTo);
// Disable button
GetDlgItem(IDOK)->EnableWindow(FALSE);
// Start off the thread
AfxBeginThread(Example1Thread, this);
}
// The thread message handler (WM_MY_THREAD_MESSAGE)
// -- #define WM_MY_THREAD_MESSAGE WM_APP+100
// -- ON_MESSAGE(WM_MY_THREAD_MESSAGE, OnThreadMessage)
LRESULT CExample1_Dlg::OnThreadMessage(WPARAM wParam, LPARAM)
{
int nProgress= (int)wParam;
// update progress bar
ProgressBar.SetPos(nProgress);
CString strStatus;
strStatus.Format("Processing item: %d", nProgress);
// update status text
StatusText.SetWindowText(strStatus);
return 0;
}
UINT Example1Thread(void *pParam)
{
CExample1_Dlg* pThis= (CExample1_Dlg*)pParam;
for (int nValue = pThis->StartFrom; nValue <= pThis->EndTo; ++nValue)
pThis->SendMessage(WM_MY_THREAD_MESSAGE, nValue);
return 0;
}
线程调用 SendMessage
并发送 WM_MY_THREAD_MESSAGE
消息(自定义消息)。这反过来会在主线程的上下文中调用 OnThreadMessage
,从而安全地更新控件!
(对于 MFC/WinAPI 程序员:这个例子可能是您理解和使用 SendMessage
、ON_MESSAGE
以及定义/处理用户消息的一个起点!您可能会觉得它很简单。我建议您阅读一些资料并自己尝试一些操作!)
这是它的样子
在继续进行另外两个实际示例之前,最后的说明。这里选择的方法(在 C++ 和 C# 中)都不合适。
- 首先,我们正在访问另一个线程中的整数变量。我们可以在线程开始之前将这些变量传递给线程,我将在下一部分详细介绍。
- 其次,我们仍然调用
SendMessage
/Invoke
而不是PostMessage
/BeginInvoke
,这实际上意味着目标线程必须在发送者线程继续之前完成。(更多关于这方面的内容将在下一节中介绍。) - 第三,线程完成后,我们没有让用户重新开始处理,因为我们禁用了按钮并且没有启用它。
- 第四,在线程运行时,用户可以关闭窗口,这可能会引发异常并导致进程崩溃!
现在,让我们继续处理这四个问题,以及允许用户取消(实际示例 2),并允许用户暂停/恢复(示例 3)!
实用示例 2
现在,您需要类似这样的东西(处理开始后)
为此,您需要使用一种同步机制来通知工作线程(具有循环的线程)。有一组同步图元,以及一组用于使用相应图元的 API 和类。在本文中,我将只讨论一种图元:Event。事件有两种类型:手动重置和自动重置,并且(在本文中)我将只涵盖手动重置事件。
请不要混淆 Windows 事件机制和线程同步事件图元。它们是不同的实体!
线程同步
事件和其他线程同步图元可以处于两种状态之一:已触发(Signaled)或未触发(Non-signaled)。将它们想象成交通信号灯,如下所示:
- 当同步对象处于已触发状态时,您可以使用该资源。就像交通信号灯是绿色时,您可以使用道路(资源)出行。
- 当同步对象处于未触发状态时,您不能使用该资源,您(线程)必须等待信号变为自由(已触发)后才能使用它。很明显,您说对了——当交通信号灯是红色时,您必须等待信号变为绿色才能移动您的车辆!
当线程等待同步对象变为已触发状态时,它被称为阻塞。下图显示了线程如何等待同步对象、使用资源,然后释放同步对象的典型用法。
图中绿色所示的方法名称不是实际的,只是指示性的。还必须注意,同步对象不是您将使用的资源。它只是保护资源不被不安全地同时访问的一种机制。
继续处理事件
事件是线程同步对象/图元之一,是最简单的同步对象之一。它们用于引发事件表明某个事件已发生,以便其他线程可以执行某些操作。线程可以设置事件(触发)、重置事件(非触发)并等待事件被触发。
下表显示了不同框架中的线程图元
内容 / 位置 | Windows API | MFC 库 | .NET Framework |
事件数据类型 | HANDLE |
CEvent 类 |
ManualResetEvent 、AutoResetEvent |
创建事件* | CreateEvent |
CEvent 构造函数 |
new ManualResetEvent 、new AutoResetEvent |
等待事件 | WaitForSingleObject |
CEvent::Lock 方法 |
WaitOne 方法 |
触发事件(信号) | SetEvent |
CEvent::SetEvent 方法 |
Set 方法 |
将事件设置为非触发状态 | ResetEvent |
CEvent::ResetEvent 方法 |
Reset 方法 |
- * 您可以创建已触发或未触发模式的事件。
请清楚地理解,使用“Wait
”成功获取同步对象,这隐含着锁定(非触发)同步对象。您无需调用“Reset
”来锁定它。您调用“Set
”来将其释放(触发)给其他线程。是的,是的,我知道您需要一个实际的示例!
这一切都是为了允许用户“取消”?
是的!我们只需要创建一个手动重置事件。将其设置为未触发(即,尚未引发事件!)。当用户单击“取消”时,我们将事件状态设置为已触发(事件已引发!)。
另一个线程如何知道?嗯,在每次迭代中,线程都会“等待”事件,以测试它是否已变为已触发。等待超时将正好是零!这意味着等待应立即返回!返回值将告诉我们是否收到了事件(信号)。如果收到信号,我们将从线程返回!(耶!线程现在可以退出了!)
这是代码(终于!)。
ManualResetEvent CancelEvent = new ManualResetEvent(false);
Thread MyThread;
private void btnStartThreaded_Click(object sender, EventArgs e)
{
// Other content is same as in previous example.
// Here 'MyThread' is moved to class-level. See comments below this code.
MyThread = new Thread(ProcessRoutine);
MyThread.Start();
}
private void btnCancelButton_Click(object sender, EventArgs e)
{
// Raise (signal) the event
CancelEvent.Set();
// Wait for the thread to finish
// (i.e. let it process the signal request, and return)
MyThread.Join(); // Wait for thread to exit!
// Notify the user (see comments after code)
lblStatus.Text = "Cancelled!";
}
void ProcessRoutine()
{
for (int nValue = StartFrom; nValue <= EndTo; nValue++)
{
// Check on each iteration if event was raised (signaled)
// that implicitly means if user requested to 'Cancel'
if (CancelEvent.WaitOne(0, false) == true)
{
return;
}
this.BeginInvoke(this.TheDelegate, nValue);
// Simulate processing, so we are sleeping for 200 milliseconds (.2s)
Thread.Sleep(200);
}
}
关于代码的注释
MyThread
已移至类级别,以便我们可以在btnCancelButton_Click
(Thread.Join
)中等待线程终止。Sleep
和BeginInvoke
是必需的,以便用户可以实际单击“取消”按钮。如果您正确运行了示例 1,您就会意识到窗体并不那么响应。
更多关于 BeginInvoke
(也适用于 PostMessage
API):发送消息(同步)意味着发送线程将阻塞直到目标完成。我们需要两个线程字面意义上并行/并发地工作。使用 Invoke
/SendMessage
通知 UI 线程(主线程)实际上会导致两个线程串行运行。这样,主线程也变得忙碌——因此,用户将无法正确单击“取消”。使用“Sleep
”,我们可以让工作线程模拟实际工作,并让主线程也空闲一段时间。这样,用户就可以单击“取消”了。但是,两个线程仍然不会并行运行。所以我选择了使用 BeginInvoke
。参见 C++ 代码之后的图像。
最后的说明:BeginInvoke
/PostMessage
会将消息发布到目标窗口,目标窗口/线程可能需要一些时间来处理它。这意味着发送线程可能在目标线程处理消息之前完全退出。最终这意味着 Thread.Join
(或 WaitForSingleObject
,如下文)可能在消息被窗口处理之前返回。这就是为什么“已取消”可能并非总是出现!
要求很高,我知道我知道!多线程需要比阅读此类文章更多的实践(但请阅读我的!)。尝试根据您的喜好更改代码,您就会了解更多细节!
这是 Visual C++ 程序员的代码
void CExample2_Dlg::OnBnClickedCancel()
{
GetDlgItem(ID_CANCEL)->EnableWindow(FALSE);
// Let thread know about this event
CancelEvent.SetEvent();
// Wait till target thread responds, and exists
WaitForSingleObject(ptrThread->m_hThread,INFINITE);
// update status text
StatusText.SetWindowText("Canceled!");
}
static UINT Example2Thread(void*);
void CExample2_Dlg::OnBnClickedOk()
{
// Get start and end values
StartFrom = GetDlgItemInt(IDC_START);
EndTo = GetDlgItemInt(IDC_END);
// Set progress bar range
ProgressBar.SetRange(StartFrom, EndTo);
// Disable button
GetDlgItem(IDOK)->EnableWindow(FALSE);
GetDlgItem(ID_CANCEL)->EnableWindow(TRUE);
// Start off the thread
ptrThread = AfxBeginThread(Example2Thread, this);
}
UINT Example2Thread(void *pParam)
{
CExample2_Dlg* pThis= (CExample2_Dlg*)pParam;
for (int nValue = pThis->StartFrom; nValue <= pThis->EndTo; ++nValue)
{
if( pThis->CancelEvent.Lock(0) )
return -1;
pThis->PostMessage(WM_MY_THREAD_MESSAGE, nValue);
Sleep(200);
}
return 0;
}
这是同步和异步通信的图形表示(同步(Synchronous)和同步化(Synchronization)是两个意义不同的词!!)
这是否意味着我们应该总是使用异步消息传递?绝对不是! 有些情况下,您需要执行串行通信,即等待目标线程/窗口处理您的请求。例如,在文件复制程序中,您必须让用户知道正在复制的正确文件名。使用异步模式,由于目标线程/窗口处理消息的延迟,您可能会误导用户认为旧文件仍在复制。最后,(一个或多个)文件名可能根本不会显示给用户,因为您的文件复制线程已经完成了复制!用户可能会重新开始复制所有文件,最终在一段时间后停止使用您的应用程序!;)
实用示例 3
您需要允许用户在取消处理的同时暂停/取消处理。
我们可以采用以下两种方法之一:
- 使用线程挂起和线程恢复功能来分别暂停和恢复处理。
- 使用适当的同步机制。
第一种方法很简单,但不推荐。
bool IsThreadPaused;
// IsThreadPaused = false; (before Thread.Start)
private void btnPause_Click(object sender, EventArgs e)
{
// Bad approach!
if (!IsThreadPaused)
{
IsThreadPaused = true;
MyThread.Suspend();
btnPause.Text = "Resume";
lblStatus.Text = "Paused!";
// Disallow Cancel
btnCancelButton.Enabled = false;
}
else
{
IsThreadPaused = false;
MyThread.Resume();
btnPause.Text = "Pause";
btnCancelButton.Enabled = true;
}
}
MFC 程序员的代码如下
void CExample3_Dlg::OnBnClickedPause()
{
if( ! m_bIsThreadSuspended )
{
m_bIsThreadSuspended = true;
ptrThread->SuspendThread();
GetDlgItem(ID_CANCEL)->EnableWindow(FALSE);
m_cPauseButton.SetWindowText("Resume");
StatusText.SetWindowText("Paused!");
}
else
{
m_bIsThreadSuspended = false;
GetDlgItem(ID_CANCEL)->EnableWindow();
m_cPauseButton.SetWindowText("Pause");
ptrThread->ResumeThread();
}
}
我将在本系列的下一部分介绍第二种暂停/恢复线程的方法。如果您能弄清楚如何做到这一点,我鼓励您这样做!
结束时间!
本文(以及可下载文件)中提供的代码并不完美。UI 设计很糟糕,无法应对用户实际使用应用程序的方式。此外,几乎所有地方都没有进行错误检查。等等。我所有的注意力都集中在文章上,并为初级多线程程序员提供关于何时以及如何使用多线程的想法。我试图使文章尽可能简单,以便每个人都能清晰地了解多线程。
如果您有疑问,请留下评论;如果您不喜欢什么,请告诉我。如果您有任何想法或问题可以帮助我撰写更好的文章,请敲击键盘!
下一部分(2010 年 4 月 5 日):多线程实用指南 - 第 2 部分。
祝您编程愉快!