[原创]理解Windows消息机制
文章作者:灰狐 (Bink&&E.S.T)信息来源:邪恶八进制信息安全团队([url]www.eviloctal.com[/url])
[b]注:文章首发[url=http://Www.HuiHu32.cN]灰狐's Blog [/url]([url]Www.HuiHu32.cN[/url]),后由原创作者友情提交到邪恶八进制信息安全团队技术论坛。[/b]
本文绝大部分内容来源于《C++ Builder 高级应用开发指南》,清华大学出版社出版,李幼仪、甘志 编著,版权归他们所有。作者仅仅对其进行了拷贝,不享有除学习之外的任何权利。
本文以及作者其他系列文章中出现的所有程序代码均经过严格的验证,如果没有特别说明即是在Win XP SP2 + C++ Builder 6.0的环境下测试通过。
一、消息的基本概念
消息(Message)指的就是Windows操作系统发给应用程序的一个通告,它告诉应用程序某个特定的事件发生了。比如,用户单击鼠标或按键都会引发Windows系统发送相应的消息。最终处理消息的是应用程序的窗口函数,如果程序不负责处理的话系统将会作出默认处理。
从数据结构的角度来说,消息是一个结构体,它包含了消息的类型标识符以及其他的一些附加信息。比如对于鼠标单击产生的消息而言,它就包含了窗口句柄、此消息的常量标识符、鼠标的位置坐标等相关信息。
Windows系统定义了许多消息常量,包括标准的Windows消息、通知消息、命令消息等等。这些消息常量通常具有XX_YYYY的形式,其他XX通常代表消息的类型,而后面的YYYY通常是这个消息所对应事件的英文缩写。比如WM_LBUTTONDOWN代表的事件就是按下了鼠标左键。
二、Windows的消息系统
Windows的消息系统由3个部分组成:
[list][li][/li][/list]消息队列。Windows操作系统本身维护了一个系统消息队列,而对于每一个正在执行的Windows应用程序,系统会为其建立一个应用程序消息队列。应用程序可以从这个消息队列中获取消息,然后分派给对应的窗口。
[list][li][/li][/list]消息循环。Windows应用程序中都包含了一段称作“消息循环(也称消息泵)”的代码,用来从消息队列中检索消息并把他们分发到相应的窗口函数中。正是这个消息循环使得一个应用程序能够响应外部的各种事件,所以消息循环往往是一个Windows应用程序的核心部分。
[list][li][/li][/list]窗口函数。最终为了处理各种消息,Windows应用程序所创建的每个窗口(广义,包括实际窗口、控件等诸如此类的的内容)都会在系统中注册一个相应的窗口函数,此窗口函数从形式上看一个巨大的switch语句,用以处理由消息循环发送到该窗口的各种消息。窗口函数是一种回调函数(Callback Function),也就是说,它是由Windows操作系统负责调用的,而应用程序本身不能调用它。
Windows操作系统中的消息从发生到被处理一般有5个步骤:
(1)系统发生了一个事件。
(2)Windows系统把事件翻译为对应的消息,并把它放到消息队列中。
(3)应用程序从消息队列中获取消息,然后把它封装在TMsg结构中。
(4)应用程序通过消息循环把消息分派给对应的窗口函数。
(5)窗口函数负责最终处理这个消息。
三、C++ Builder中的OnMessage事件
C++ Builder为了方便消息处理,进一步把常用的消息封装成了各种相应事件,这样程序员通常情况下就无需考虑消息的具体细节,只要编写相应的事件处理函数即可。这就是所谓的基于事件的Windows程序开发。
在C++ Builder开发的应用程序中,任何窗体接收到一个Windows消息都会触发一次OnMessage事件,所以可以通过响应TApplication对象的OnMessage事件来捕获任何发送给本程序的Windows消息。
OnMessage事件的处理函数原型如下:
typedef void __fastcall (__closure *TMessageEvent)(tagMSG &Msg,bool &Handled);
这个函数有两个参数,其中参数Msg表示的是被截获的消息,而参数Handled则用来指示本消息是否已经处理完成。在程序中可以通过设置参数Handled为true以避免后续过程处理这个消息,反之亦然。
需要注意的是,OnMessage事件仅仅接受发送到消息队列中的消息,而直接使用API函数 SendMessage()发送给窗口函数的消息将不会被截获到。另外,当程序运行的时候,OnMessage事件被触发的频率一般非常高,所以这个事件处理函数中的代码执行事件将直接影响到整个程序的运行效率。
下面我们通过一个实例来演示OnMessage事件的处理过程。在这个范例中,程序将累计发生的OnMessage事件次数并显示在窗体上。
我们在主窗体上放置一个Label组件,命名为Label1,用于显示消息发生次数。
程序中全部采用默认名字。
在窗体类头文件Unit1.h中添加以下代码:
private: // User declarations
long num;
void __fastcall AppMessage(tagMSG &Msg,bool &Handled);
然后在Unit1.cpp中添加以下代码:
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
num=0;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
Application->OnMessage=AppMessage;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::AppMessage(tagMSG &Msg,bool &Handled)
{
num++;
Label1->Caption=AnsiString(num);
Handled=false;
}
//---------------------------------------------------------------------------
其中Application->OnMessage=AppMessage;的作用是把事件处理函数AppMessage()和OnMessage事件联系起来。Handled=false;是为了让后续过程能继续处理消息,以避免窗口无法正常接收消息而引起死锁。
最后按F9运行程序,可以看到窗体上显示出了消息事件发生的次数,当你移动鼠标或按下按键的时候它都会不断地变化。
本文到此结束,敬请关注续篇:Windows消息高级应用。 本文是《理解Windows消息机制》的续篇,在阅读本文前需要您先理解《理解Windows消息机制》一文中的内容。
四、利用消息映射截获消息
C++ Builder提供了一种消息映射机制,通过消息映射程序能将特定的Windows消息与对应的消息处理函数联系起来,当窗口捕获到这个消息的时候就会自动调用对应的处理函数。
在程序中使用这样的消息映射一般需要以下三个步骤:
(1)声明消息映射表,把某些消息的处理权交给自定义的消息处理函数。
这样的消息映射列表应该位于一个组件类的定义中,它以一个没有参数的BEGIN_MESSAGE_MAP宏开始,以END_MESSAGE_MAP宏结束。END_MESSAGE_MAP宏的唯一参数应该是组件的父类的名字。通常情况下这个所谓的父类指的就是TForm。在宏BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间可以插入一个或者是多个MESSAGE_HANDLER宏。
MESSAGE_HANDLER宏将一个消息句柄和一个消息处理函数联系在了一起。它有三个参数:Windows消息名、消息结构体名和对应的消息处理函数名。其中,消息结构体名既可以是通用的消息结构体TMessage,也可以是特定的消息结构体比如TWMMouse。
在使用消息映射的时候应该注意以下两点:
一个窗口类定义中只能有一个消息映射列表;
消息映射必须位于它所引用的所有消息处理函数声明的后面。
一个典型的消息映射声明代码如下:
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(Windows消息名,消息结构体名,消息处理函数名)
MESSAGE_HANDLER(Windows消息名,消息结构体名,消息处理函数名)
...
END_MESSAGE_MAP
(2)在窗口类中声明消息处理函数
注意,这里的消息处理函数的名字和参数都必须和对应的MESSAGE_HANDLER宏一致。
一个典型的消息处理函数声明如下:
void __fastcall 消息处理函数名(消息结构体名 &Message)
(3)实现消息处理函数
消息处理函数的编制和普通的窗口类成员函数没有太大的差异,唯一不同的是,通常在此函数的最后都要加上一句:TForm::Dispatch(&Message),以完成VCL对于消息的默认处理。如果没有这条语句,消息将会被完全拦截;这样可能造成某些情况下VCL类因得不到消息而无法正常工作。
下面我们通过一个实例来演示如何通过消息映射在程序中截获Windows消息。在这个范例中,程序将强制设置主窗体的最大、最小尺寸。
我们仍然采用默认命名,将主窗体Form1的Height和Width属性分别设置为150和250。在示例程序中我们将要限制窗体的最大长、宽分别为300和200,最小长、宽分别为200和100。
编辑窗体类的头文件Unit1.h如下:
class TForm1 : public TForm
{
__published: // IDE-managed Components
TLabel *Label1;
private: // User declarations
public: // User declarations
__fastcall TForm1(TComponent* Owner);
void __fastcall WMGetMinMaxInfo(TWMGetMinMaxInfo &Msg);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_GETMINMAXINFO,TWMGetMinMaxInfo,WMGetMinMaxInfo)
END_MESSAGE_MAP(TForm)
};
在窗体文件Unit1.cpp中只需要实现一个函数,其他函数全部不写任何代码。
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
void __fastcall TForm1::WMGetMinMaxInfo(TWMGetMinMaxInfo &Msg)
{
//当用户单击最大化按钮时,限制其大小
Msg.MinMaxInfo->ptMaxSize.x=300;
Msg.MinMaxInfo->ptMaxSize.y=200;
//设定窗体最大化时左上角的屏幕坐标为当前窗体的位置
Msg.MinMaxInfo->ptMaxPosition.x=Left;
Msg.MinMaxInfo->ptMaxPosition.y=Top;
//当用户用鼠标拖动改变窗体尺寸时,限制其最大值
Msg.MinMaxInfo->ptMaxTrackSize.x=300;
Msg.MinMaxInfo->ptMaxTrackSize.y=200;
//当用户用鼠标拖动改变窗体尺寸时,限制其最小值
Msg.MinMaxInfo->ptMinTrackSize.x=200;
Msg.MinMaxInfo->ptMinTrackSize.y=100;
//显示当前窗体大小尺寸
Label1->Caption="Width="+AnsiString(Width)+" Height="+AnsiString(Height);
}
//---------------------------------------------------------------------------
程序代码中的TWMGetMinMaxInfo结构的声明在头文件Messages.hpp中,MINMAXINFO结构在WinUser.h中有详细说明。
F9运行程序后完全符合我们预先的设想。
关于消息映射的实现细节,我们可以通过在头文件$(BCB)\Include\Vcl\sysmac.h中的这些宏的代码来搞清楚。
五、重载WndProc()函数
在上一节我们学会了使用消息映射来捕获或屏蔽某些特定的消息,当然,那种方法并不是唯一的,我们还可以通过重载窗口函数WndProc()来实现。因为系统会在调用函数Dispatch()分派消息之前调用窗口函数WndProc(),所以我们可以通过重载此函数得到一个在分派消息之前就处理消息的机会。
这个用于处理消息的窗口函数原型如下:
virtual void __fastcall WndProc(Message::TMessage &Message);
下面我们就通过一个实例来演示如何重载它,要实现的目标和上面那个程序一样,只发代码就可以了,工程设置也是一样的。
编辑窗体类的头文件Unit1.h如下:
class TForm1 : public TForm
{
__published: // IDE-managed Components
TLabel *Label1;
private: // User declarations
void __fastcall WndProc(TMessage &Message);
public: // User declarations
__fastcall TForm1(TComponent* Owner);
};
编辑窗体函数文件Unit1.cpp如下:
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
void __fastcall TForm1::WndProc(TMessage &Message)
{
if(Message.Msg == WM_GETMINMAXINFO)
{
LPMINMAXINFO lpmni = (LPMINMAXINFO) Message.LParam;
lpmni->ptMaxSize.x=300;
lpmni->ptMaxSize.y=200;
lpmni->ptMaxPosition.x=Left;
lpmni->ptMaxPosition.y=Top;
lpmni->ptMaxTrackSize.x=300;
lpmni->ptMaxTrackSize.y=200;
lpmni->ptMinTrackSize.x=200;
lpmni->ptMinTrackSize.y=100;
Label1->Caption="Width="+AnsiString(Width)+" Height="+AnsiString(Height);
}
TForm::WndProc(Message);
}
//---------------------------------------------------------------------------
程序首先判断被处理的消息是否为WM_GETMINMAXINFO,如果是才进行后续处理。代码LPMINMAXINFO lpmni = (LPMINMAXINFO) Message.LParam;的作用是把消息附带的MINMAXINFO类型的结构体的地址赋值给一个工作变量lpmni。这个MINMAXINFO类型的结构体被用来存储有关窗体最大化、最小化以及窗体位置的信息。对于WM_GETMINMAXINFO而言,这个指针被存储在TMessage结构的LParam成员域中。
按F9运行程序,运行结果同样符合了我们的预期。
六、非标准消息
1,通知消息
通知消息(Notification message)指的是,当一个窗体的子控件发生了一些事情后,它通知给其父窗体的消息。需要注意的是,通知消息只发生在一些标准的Windows控件上,包括按钮、编辑框、列表框、组合列表框、树状视图控件、列表控件等。
通知消息如同标准的Windows消息一样,也可以通过消息映射等方法来进行处理。
2,自定义消息
自定义消息一般有两种方式:直接定义消息常量或调用API函数RegisterWindow-Message()向系统注册一个消息。其中比较简洁的方式是直接在程序中定义消息的数值,通常将其定义为WM_USER+XXX或者是WM_APP+XXX。Windows对于消息编号的分配情况通常如下所示:
范围 说明
0到WM_USER-1 系统消息
WM_USER到0x7FFF 为用户的窗体类保留整数类型消息
WM_APP到0xBFFF 为应用程序保留的消息
0xC000到0xFFFF 为应用程序保留的字符类型消息,由函数RegisterWindowMessage()分配
0xFFFF以上 为将来的系统应用保留
一个典型的消息常量定义如下:
#define WM_MYNOTIFY (WM_APP+100)
这种直接定义消息的方式虽然简洁,但在有的情况下,如果有大量的组件和应用使用这种方式来定义自己的消息编号时就有可能引起冲突。此时,可以通过函数RegisterWindowMessage()来获取一个系统中唯一的消息编号。
函数RegisterWindowMessage()的原型定义如下:
UINT RegisterWindowMessage(LPCTSTR lpString);
这个函数需要传输一个以null结束的字符串,并且返回一个范围是0xC000~0xFFFF的消息常量。使用它的好处在于,对于任何给定的字符串都可以得到一个系统中惟一的消息常量,从而可靠地避免了自定义消息之间可能的冲突。当然,这个函数的返回值只有在程序运行时才有意义。
3,VCL内部消息
VCL自身也使用了许多内部的消息,它们在一般的程序开发中很少用到,但是在编写自己的组件时却非常有用。这些VCL内部消息都具有CM_XXXX的形式,用于处理VCL内部的事务。
七、自己发送消息
在C++ Builder中提供了几种自己发送消息的途径:使用函数TControl::Perform()或者API函数SendMessage()和PostMessage()向特定窗体发送消息,或者是使用函数TWinControl::Broadcast()和API函数BroadcastSystemMessage()广播消息。
1,Perform()
Perform()函数适用于所有由TControl类派生的对象,可以通过这个函数直接向这些组件发送消息,而这个组件对于这个消息的反应就如同它真的从系统接收到了这个消息一样。函数Perform()的原型如下:
int __fastcall Perform(Cardinal Msg,int WParam,int LParam);
其中参数Msg表示的就是这个消息的标识符,比如WM_LBUTTONBLCLK。另外两个代表的是此消息的两个附带参数,它们对于不同的消息具有不同的含义。
调用了此函数之后,要等到消息处理之后才返回。
在同一个应用程序的不同窗体和控件之间使用Perform()是非常便捷的。但这个函数是TControl类的成员函数。也就是说,程序必须知道这个接收消息的控件类的实例。而在许多情况下程序并不知道接收消息的窗体的实例而只是知道它的句柄,比如在不同程序的窗体之间发送消息就属于这种情况。这时就要使用下面的两个函数了。
2,SendMessage()和PostMessage
这两个函数的功能基本一样,它们都可以用来向一个特定的窗口句柄发送消息。主要的区别是:函数SendMessage()直接把一个消息发送给窗口函数,等消息被处理之后才返回;而函数PostMessage()则只是把消息发送到消息队列然后就立即返回。它们的用法与Perfoem()基本相似,不过多了个hWnd参数来表示目标窗口的句柄。
3,Broadcast()和BroadcastSystemMessage()
函数Broadcast()适用于所有由TWinControl类派生的对象,它可以向窗体上所有的子控件广播消息,原型如下:
void __fastcall Broadcast(void *Message);
它只有一个参数,指向被广播的TMessage类型的消息结构体。
函数Broadcast()只能向C++ Builder应用程序中的指定窗体上所有的子控件广播消息,如果要向系统中的其他应用程序或者窗体广播消息,它就无能为力了。这时可以使用API函数BroadcastSystemMessage(),这个函数可以向任意的应用程序或者组件广播消息。它的原型如下:
long BroadcastSystemMessage(
DWORD dwFlags,
LPDWORD lpdwRecipients,
UINT uiMessage,
WPARAM wParam,
LPARAM lParam);
更详细的用法请查阅帮助文档。
当然,在程序中需要给系统中的所有进程广播消息的情况是很少见的,所以它的使用并不多。但是在程序内部给一个窗体的所有子控件广播消息的情况则较为普遍,特别是当窗体的子控件很多的情况下,用广播消息的方式显得非常便捷。(全文完) 本来想单独发帖的,想想还是算了,跟在这里比较好些,毕竟只是学习笔记而已,有扰了。
多线程编程是提高系统资源利用率的一种常见方式。它占用的资源更小,启动更快,还可以实现在后台运行一些需时较长的操作。
一、初识TThread对象
VCL 提供了用于多线程编程的TThread类,在这个类中封装了Windows关于线程机制的Windows API,通常将它的实例成为线程对象。线程对象通过封装简化了多线程应用程序的编写。注意,线程对象不允许控制线程堆栈的大小或安全属性。若需要控制这些,必须使用Windows API的CreateThread()或BeginThread()函数。不过,即使是使用Windows Thread API函数建立和控制多线程,仍然可从一些同步线程对象或下节将要描述的方法中受益。
要在应用程序中使用线程对象,必须创建 TThread的一个派生类。File|New|Thread Object,系统会提示为新线程对象提供类名,我们将其命名为TMyThread。我们必须自行在构造函数以及Execute()函数中添加代码。自动生成的构造函数中有一个参数,如果为true的话线程创建后将进入挂起状态,直到线程对象的Resume()函数被调用才开始执行。如果为false则线程创建后会立刻开始执行。
我们先创建一个实例来亲自感受下多线程:在窗体上放两个Button和两个Edit组件,自动命名。然后File|New|Thread Object来创建一个线程对象,命名为TMyThread。
以下请看完整工程代码:
//Unit1.h //主窗体头文件
//---------------------------------------------------------------------------
#ifndef Unit1H
#define Unit1H
//---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include "Unit2.h"
//---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published: // IDE-managed Components
TButton *Button1;
TButton *Button2;
TEdit *Edit1;
TEdit *Edit2;
void __fastcall Button1Click(TObject *Sender);
void __fastcall Button2Click(TObject *Sender);
void __fastcall FormCreate(TObject *Sender);
private: // User declarations
TMyThread *thread1,*thread2;
public: // User declarations
__fastcall TForm1(TComponent* Owner);
};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif
//Unit1.cpp //主窗体实现文件
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
thread1->Resume(); //单击后才启动线程
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
thread2->Resume();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
thread1=new TMyThread(true,Edit1); //创建线程对象实例
thread2=new TMyThread(true,Edit2);
}
//---------------------------------------------------------------------------
//Unit2.h //线程类头文件
//---------------------------------------------------------------------------
#ifndef Unit2H
#define Unit2H
//---------------------------------------------------------------------------
#include <Classes.hpp>
//---------------------------------------------------------------------------
class TMyThread : public TThread
{
private:
TEdit *edResult; //自定义局部变量
String strResult;
protected:
void __fastcall Execute();
void __fastcall ShowResult(); //自定义函数
public:
__fastcall TMyThread(bool CreateSuspended,TEdit *AEdit); //注意:修改了默认参数
};
//---------------------------------------------------------------------------
#endif
//Unit2.cpp //线程类实现文件
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include "Unit2.h"
#pragma package(smart_init)
//---------------------------------------------------------------------------
__fastcall TMyThread::TMyThread(bool CreateSuspended,TEdit *AEdit)
: TThread(CreateSuspended)
{
edResult=AEdit;
}
//---------------------------------------------------------------------------
void __fastcall TMyThread::Execute()
{
for(int i=0;i<200;i++)
{
strResult=IntToStr(i);
Synchronize(ShowResult); //管理线程同步,保证安全性
Sleep(100);
}
}
//---------------------------------------------------------------------------
void __fastcall TMyThread::ShowResult()
{
edResult->Text=strResult;
}
//---------------------------------------------------------------------------
然后我们F9运行程序就可以查看效果了。
二、编写线程函数
Execute ()函数就是线程函数,它包含了程序中所有需要并行执行的代码。除了共享相同的进程空间外,可以认为线程就是通过应用程序启动的程序。但在编写线程函数的时候需要注意与单独程序的不同之处。因为线程与其他线程共享内存空间,所以必须确认没有覆盖应用程序中其它线程的内存地址。而另一方面,可以使用共享内存在线程之间进行通信。
在线程函数内部,我们可以使用任意的全局变量,但有些变量我们并不希望同一线程类的其他实例共享它,就可以声明一个线程(thread-local)变量。通过将__thread修饰语加入变量声明就可以声明一个线程变量。例如 int __thread x; 声明一个整型变量。
__thread修饰语只可用于全局(文件范围)或静态变量。指针和函数变量不能作为线程变量。使用“在写入时复制”语法的类,如AnsiStrings也不能作为线程变量。需要在运行时进行初始化或析构的类型也不能被声明为__thread类型。
当程序中调用Resume()函数时,线程启动并继续执行直到Execute()结束。这就是线程执行特定任务,并在其完成时终止的模式。然而,有时应用程序需在一些外部条件满足时终止线程。通过检查Terminated属性可允许其它线程通知本线程终止。当其它线程试图终止本线程时,它调用 Terminate()函数。Terminate()函数将本线程的Terminated属性设置为true。Execute()函数通过检查和响应 Terminated属性来实现Terminate()函数。下面的实例演示了这种做法:
void __fastcall TMyThread::Execute()
{
while( !Terminated )
{
}
}
在线程函数终止时我们可能需要做一些清理工作。由于在线程终止前,OnTerminate事件会发生,所以我们可以将清理代码放在OnTerminate事件处理程序中,这样可确保不管Execute()函数如何执行,清理代码总是可以被执行。要注意OnTerminate事件处理程序不作为线程的一部分运行,而是在应用程序的主线程中执行的。这意味着:
(1)在OnTerminate事件处理程序中不能使用任何线程局部变量。
(2)在OnTerminate事件处理程序中可安全地访问任何组件及VCL对象而不会和其他线程发生冲突。
三、协调线程
在编写线程执行时运行的代码时,必须考虑到可能同步执行的其他线程行为。主要有两种情况:一个是避免两个线程试图同时使用某一个全局对象或变量;另一个是某线程中的一些代码可能会依赖其他线程所执行任务的结果。
1,避免同时访问
为避免在访问全局对象或变量时与其他线程发生冲突,可能需要暂停其他线程的执行,直到该线程代码完成操作。这里需要注意,不要暂停其他不需停止的线程执行,这样会使效率严重降低,也无法获得使用多线程的优点。
<1>,锁定对象
一些对象内置了锁定功能,以防止其他线程使用该对象的实例。例如,画布对象(TCanvas及其派生类)有一种Lock()函数可防止其他线程访问画布,直到调用Unlock()函数。
VCL还包含一种线程安全的列表对象TThreadList。调用TThreadList::LockList()返回列表对象,同时组织其他线程使用列表直到调用UnlockList()函数。调用TCanvas::Lock()函数或TThreadList::LockList()函数时可以安全地嵌套。锁定直到最后一个锁定调用匹配到同一线程中相应的解锁调用时才会被释放。
显然这种方法只对部分类有效。
<2>,使用重要区段
若对象没有提供内置的锁定功能,可使用重要区段。重要区段像门一样,每次只允许一个线程进入。要使用它,需创建TCriticalSection的全局实例。TCriticalSection有两个函数:Acquire()(阻止其他线程执行该区段)以Release()(取消对其他线程的阻止)。
每个重要区段都与需要保护的全局内存关联。每个要访问这个全局内存的线程首先要调用Acquire()函数以确保其他线程不能访问它。当线程结束时,要调用Release()函数以便其他线程能继续访问。
例如,应用程序有一个全局重要区段变量pLockXY,可阻止访问全局变量X和Y。任何使用X或Y的线程必须调用重要区段,如下所示:
pLockXY->Acquire();
try{
Y=sin(X);
}
__finally
{
pLockXY->Release();
}
<3>,使用多重读、独占写的同步器
当使用重要区段来保护全局内存时,每次只有一个线程可以使用该内容。这种保护可能超出了需要,特别是当有一个经常读但很少写的对象或变量时更是如此。多个线程同时读相同内存但没有线程写内存是没有危险的。当有一些经常被读但很少有线程向其写入的全局内存时,可使用 TMultiReadExclusiveWriteSynchronizer对象保护它。这个对象与重要区段一样,但它允许多个线程同时读,只要没有线程写即可。线程必须有独占访问权才能写使用TMultiReadExclusiveWriteSynchronizer保护的内存。
要使用“多重读、独占写”的同步器,需创建TMultiReadExclusiveWriteSynchronizer的一个全局实例,它与要保护的全局内存关联。每个需要读内存的线程首先要调用BeginRead()函数。它确保当前无其它线程写内存。线程完成读操作后调用EndRead()函数。任何线程要写内存的时候必须先调用BeginWrite()函数,结束后调用EndWrite()函数。
<4>,共享内存的其他技术
当使用VCL对象时,使用主VCL线程来执行代码,可确保对象不会间接地访问同时被其他线程中的VCL对象使用的内存。若全局变量不需要被多个线程共享,可使用线程变量来代替它。线程可以不需要等待或暂停其他线程。
2,等待其他线程
若线程必须等待另一线程完成某项任务,可让线程临时中断执行。然后要么等待另一线程完全执行结束,要么等到另一线程通知完成了该项任务。
<1>,等待线程执行结束
要等待另一线程执行结束,使用它的WaitFor()函数。WaitFor()函数直到那个线程终止才返回,终止的方式要么完成了Execute()函数,要么由于一个异常。例如,下面的代码在访问列表中的对象前等待,直到另一线程填满该列表。
void __fastcall TVisitList::Execute()
{
int fileRes;
TFillThread *fl=new TFillThread(false);
fillRes=f1->WaitFor();
//以下进行后续处理
}
上例中,列表对象只在WaitFor()函数指出该列表被填满时才能被访问。返回值由被等待线程的Execute()函数指定。然而,因为调用 WaitFor()函数的线程需要直到另一线程的执行结果,无法以代码调用Execute()函数,Execute()函数也无法返回任何值。所以 TFillThread线程的Execute()函数应该设置ReturnValue属性。ReturnValue通过被其他线程调用的WaitFor ()函数返回。返回值是一个证书,由应用程序确定其含意。
<2>,等待任务完成
有时,只需等待线程完成一些操作而不是执行结束。为此,可使用一个事件对象。事件对象(TEvent)应具有全局范围以便他们能够为所有线程可见。当一个线程完成一个被其他线程依赖的操作时,调用TEvent::SetEvent()函数。它发出一个信号,以便任何其他线程可检查并得知操作完成。要关掉信号则使用ResetEvent() 函数。
四、调试多线程程序
当调试多线程应用程序时,试图跟踪所有并行线程的状态,或在断点停止时判断是哪一个线程的执行往往会使人感到迷惑。可使用Thread Status框来帮助跟踪并控制应用程序中所有的线程。开启Thread Status框的方法是:View|Debug Windows|Threads。
当一个调试事件(断点、异常、暂停)发生时,线程状态指示各个线程的状态。右击可定位相应的源代码位置或将其他线程设置为当前线程等。
最后要提醒的是,不要使用无意义的多线程。如果一段程序完全是串行的,每一步的操作都需要上一步的结果,那么在这里采用多线程技术就是毫无意义的。
页:
[1]