http://www.thebits.org/tutorials/mjsema1.asp
指南:工作者线程和信号量
©Malcolm Smith, 14th October 2002
创建工作者线程并等待它们结束
那么在是何种境况下您可以创建使应用程序冻结直到所有的线程完成了它们的处理的线程呢?
通常的情景是您的应用程序开始多线程,在转到下一个独立的任务前,需要等待所有的这些线程完成时。
本文我将使用和前面一样的线程类,创建一些实例并等待它们结束。
下面我进行深入讨论(一些代码将被省略):
for(int i = 0; i < 10; i++)
{
Counter++;
LabelIndex = (Counter-1) % 6;
TWorkerThread *Thread = new TWorkerThread(true, LabelArray[LabelIndex], 50);
Thread->FreeOnTerminate = true;
Thread->Resume();
}
上面同样的代码创建10个线程(现在请忽略它们都有同样标签的事实)
我们无法得知所有的线程何时全部完成。这可以通过在另外的线程中创建这些独立的线程,使用一个知道有多少当前运行线程的ThreadCounter来实现。
这就是上面的线程类被称为工作者线程的地方。下面我创建一个主人线程,通过该线程创建这些工作者线程,使用计数器去判断它们何时都结束了。
注意,这里最初的代码将会导致前面提到的死锁。更正的版本稍后会涉及。
这是类定义:
class THostThread : public TThread
{
private:
int Counter;
int MaxThreads;
TLabel* LabelArray[6];
protected:
void __fastcall Execute(void);
void __fastcall HasCompleted(TObject *Sender);
public:
__fastcall THostThread(bool CreateSuspended, TLabel* ALabelArray[6],
int AMaxThreads);
};
此刻先别管 AmaxThreads参数。
该类将创建10个线程,并等待它们全部完成。实现如下:
__fastcall THostThread::THostThread(bool CreateSuspended, TLabel *ALabel,
int AMaxThreads) : TThread(CreateSuspended), pLabel(ALabel)
{
Counter = 0;
MaxThreads = AMaxThreads;
memcpy(LabelArray, ALabelArray, sizeof(TLabel*) * 6);
}
void __fastcall THostThread::Execute(void)
{
int LabelIndex;
// 创建10个线程 ,增加我们的计数器
for(int i = 0; i < 10; i++)
{
Counter++;
LabelIndex = (Counter-1) % 6;
TWorkerThread *Thread = new TWorkerThread(true, LabelArray[LabelIndex], 50);
Thread->FreeOnTerminate = true;
Thread->OnTerminate = HasCompleted; // method used to decrement counter
Thread->Resume();
}
// 等待所有线程结束
while(!Terminated && Counter > 0)
Sleep(1);
}
void __fastcall THostThread::HasCompleted(TObject *Sender)
{
Counter--;
}
现在代码看起来很好,Execute方法创建10个线程,每个线程结束后让计数器减1。Execute方法直到所有的线程都结束才返回。
完整起见,您可能需要这样实现这个线程的创建。
THostThread *Thread = new THostThread(true, LabelArray, 3);
Thread->FreeOnTerminate = true;
Thread->Resume();
Thread->WaitFor();
猜猜会发生什么?您的应用程序在最后一行会产生难挨的中断。因为主VCL线程被挂起直至主人线程“Thread”返回。工作者线程仍然尝试通过同步去更新主VCL线程(此时是挂起的)的标签。当您根本不用同步但又使用OnTerminate 事件时类似的问题也会发生。这个事件被主VCL线程的上下文调用从而导致同样的问题。
那么您如何克服这个问题呢?
基本上,您需要设计您的GUI(或者业务对象),可以让它继续处理消息,但是它要知道在您所创建的线程告知它时才做相应的操作。
下面我展示一下在一个GUI应用程序中使用TActionList如何这么做。
演示程序中标名“Run Host Thread”的按纽是和ActionList组件关联的。当您双击这个动作列表组件时,您会看到HostThreadButton项。我使用它的唯一目的就是激活或禁用上面提到的按钮。在一个大的应用程序中您可能还有您的主人线程运行时需要禁用的菜单项或者其他控件。将这些控件都分配给这个动作,您可以通过一行代码去激活或禁用它们。(例如:)
HostThreadButton->Enabled = false;
上例中,所有连接到这个动作的控件都将被禁用。
演示程序中的按纽自身被btnHostThread调用,它的Action属性指向HostThreadButton。这样当HostThreadButton被禁用,连接它的按钮同时被禁用。如果您看btnHostThread按钮的OnClick事件,您会看到没有指定事件处理。那么当按钮被按下时,代码是如何执行的呢?
我先前声明这个按钮的action属性制向HostThreadButton动作。您看这个动作,它有一个OnExecute事件,无论何时点击按钮,这个方法都会被调用。 如果我们有和这个动作关联的菜单项,那么菜单项被选上时同样的代码也会运行。
这是这个动作的OnExecute事件的部分代码(没有注解)
THostThread *Thread = new THostThread(true, LabelArray, 3);
Thread->FreeOnTerminate = true;
Thread->OnTerminate = HostThreadHasCompleted;
Thread->Resume();
HostThreadButton->Enabled = false;
这时,我们已经通知主人线程在它结束时去调用HostThreadHasCompleted方法。猜猜下面我们要做什么?
void __fastcall TForm1::HostThreadHasCompleted(TObject *Sender)
{
HostThreadButton->Enabled = true;
}
这么做没有奖品。 如果您运行演示程序的这部分,您会看到6个标签被更新,按钮最初被禁用,当所有的线程完成后被重新激活。
由于演示程序的简单,您看不到10个线程在运行,因为我们只有6个标签.
下面我们继续进一步修改代码。让它变成这个样子:让10个线程最终都运行,但同时最多只有3个线程在运行。我们的按钮直到10个线程完全完成才能被激活。
我们要添加一个信号量到我们的主人类。信号量是个同步对象,我们用它来限制创建的线程的数目。这个对象通过处理它自己的计数器来工作。任何时候,计数器的值大于0就允许执行线程,当计数器等于0时,线程会中止,直到一个工作者线程结束增加了信号量的计数器。
请这么做。
添加信号量句柄到THostThread类,在构造时创建这个信号量。
hSemaphore = CreateSemaphore(NULL, 3, 3, NULL);
这创建了一个初始量为3、最大数目为3的信号量。接着,我们修改这个线程类的Execute方法,如下:(略去演示程序中的 try/__finally 块)
void __fastcall THostThread::Execute(void)
{
int LabelIndex = 0;
for(int i = 0; i < 10; i++)
{
Counter++;
LabelIndex = LabelIndex % 6;
::WaitForSingleObject(hSemaphore, INFINITE);
TWorkerThread *Thread = new TWorkerThread(true, LabelArray[LabelIndex], 50);
Thread->FreeOnTerminate = true;
Thread->OnTerminate = HasCompleted; // 用以减小计数器的方法
Thread->Resume();
LabelIndex++;
}
while(!Terminated && Counter > 0)
Sleep(1);
CloseHandle(hSemaphore);
}
自然,每个线程结束时调用的方法也要做相应变动。
void __fastcall THostThread::HasCompleted(TObject *Sender)
{
Counter--;
::ReleaseSemaphore(hSemaphore, 1, NULL);
}
这代码用以指引标签数组中下一项被修改,因为计数器的值从来不会大于2。
调用WaitForSingleObject减少信号量处理的计数器。只要这个计数器大于0,线程就会继续执行。
最初的循环创建了3个工作者线程,信号量计数器等于0,然后线程的执行停在WaitForSingleObject那里。
任何一个工作者线程结束,HasCompleted方法被调用。ReleaseSemaphore调用再次增加信号量计数器从而允许主人线程继续执行。另一个工作者线程被创建。停止/继续/停止这样的过程一直进行直至最终所有10个工作者线程都被创建并完全执行。当最后的线程结束时,Execute方法完成,GUI的按钮再次被激活。
运行演示程序,您会看到先是前面3个标签被更新,接着是后面3个,再接着是前面3个,最后是第4个(正好10个线程)。
一旦您明白了,它就什么都不是了。
摘要
这篇短文向您展示了如何创建简单的线程,并与主VCL线程同步事件。接着我们在一个主人线程中创建一些工作者线程,展示如何避免您的应用程序死锁(哦,至少使您知道了典型的原因)。
最后,给出了一种使用信号量控制运行中的线程数的方法。
致礼
(翻译:01soft)