首页 > 代码库 > delphi线程同步
delphi线程同步
本文完全摘自网络,仅供自己查询
上次跟大家分享了线程的标准代码,其实在线程的使用中最重要的是线程的同步问题,如果你在使用线程后,发现你的界面经常被卡死,或者无法显示出来,显示混乱,你的使用的变量值老是不按预想的变化,结果往往出乎意料,那么你很有可能是忽略了线程同步的问题。
当有多个线程的时候,经常需要去同步这些线程以访问同一个数据或资源。例如,假设有一个程序,其中一个线程用于把文件读到内存,而另一个线程用于统计文件中的字符数。当然,在把整个文件调入内存之前,统计它的计数是没有意义的。但是,由于每个操作都有自己的
线程,操作系统会把两个线程当作是互不相干的任务分别执行,这样就可能在没有把整个文
件装入内存时统计字数。为解决此问题,你必须使两个线程同步工作。存在一些线程同步地
址的问题,Windows 提供了许多线程同步的方式。在本节您将看到使用临界区、互斥、信
号量、事件、全局原子和Synchronize 函数来解决线程同步的问题。
下面的同步技术一般均有两种使用方式,一种是直接使用Windows API 函数,一种是使用
由Delphi 对API 函数进行封装的类。
以下函数以Delphi 2009 中的函数格式为准。
1. Critical Sections 临界区
临界区是一种最直接的线程同步方式。所谓临界区,就是一次只能由一个线程来执行的一段
代码。例如把初始化数组的代码放在临界区内,另一个线程在第一个线程处理完之前是不会
被执行的。临界区非常适合于序列化对一个进程中的数据的访问,因为它们的速度很快。
(1). 使用EnterCriticalSection( ) 和LeaveCriticalSection( ) API 函数
在使用临界区之前, 必须定义一个TRTLCriticalSection 类型的记录变量并使用
InitializeCriticalSection( ) 过程来初始化临界区。该过程多半在窗体创建时或在程序初始化时
执行。
其声明如下:
procedure InitializeCriticalSection(var lpCriticalSection : TRTLCriticalSection);stdcall;
lpCriticalSection 参数是一个TRTLCriticalSection 类型的记录, 并且是变参。至于
TRTLCriticalSection 是如何定义的,这并不重要,因为很少需要查看这个记录中的具体内容。
只需要在lpCriticalSection 中传递未初始化的记录, InitializeCriticalSection( ) 过程就会填
充这个记录。
注意:Microsoft 故意隐瞒了TRTLCriticalSection 的细节。因为,其内容在不同的硬件平台
上是不同的。在基于Intel 的平台上,TRTLCriticalSection 包含一个计数器、一个指示当前
线程句柄的域和一个系统事件的句柄。在Alpha 平台上,计数器被替换为一种Alpha-CPU数据结构,称为spinlock 。
在记录被填充后,我们就可以开始创建临界区了。这时我们需要用EnterCriticalSection( ) 和
LeaveCriticalSection( ) 来封装代码块,这两个函数分别代表进入和离开临界区,将要同步的
代码块放在这两个函数中间。在第一个线程调用了EnterCriticalSection( ) 之后,所有别的
线程就不能再进入代码块并挂起等待第一个线程离开临界区。下一个线程要等第一个线程调
用LeaveCriticalSection( ) 后才能被唤醒。这两个过程的声明如下:
procedure EnterCriticalSection(var lpCriticalSection : TRTLCriticalSection);stdcall; //进入临界区
procedure LeaveCriticalSection(var lpCriticalSection : TRTLCriticalSection);stdcall; //离开临界
区
正如你所想的,参数lpCriticalSection 就是由InitializeCriticalSection( ) 填充的记录。
如果在某个子线程执行EnterCriticalSection( ) 前,已经有另一个线程进入临界区且还未离
开临界区,则该子线程将挂起并无限期等待另一个线程离开临界区,要想不挂起且0 时间
等待,必须使用TryEnterCriticalSection( ) 。该过程声明如下:
function TryEnterCriticalSection(var lpCriticalSection: TRTLCriticalSection): BOOL; stdcall;
TryEnterCriticalSection( ) 不同于EnterCriticalSection( ) 的声明在于多出一个布尔型的返回
值,如果返回True 代表成功进入临界区,如果返回False 代表临界区已占用且不进入临界
区。运用这个函数,线程能够迅速查看它是否可以访问某个共享资源,如果不能访问,那么
它可以继续执行某些其他操作,而不必进行等待。
使用TryEnterCriticalSection( ) ,必须判断其返回值。
当你不需要临界区时,应当调用DeleteCriticalSection( ) 过程删除临界区,该函数多半在窗
体销毁时或程序终止前执行。下面是它的声明:
procedure DeleteCriticalSection(var lpCriticalSection : TRTLCriticalSection); stdcall;
例:
type
TMyThread = class(TThread)
protected
procedure Execute; override;
public
constructor Create; virtual;
end;
var
Form1 : TForm1;
CriticalSection : TRTLCriticalSection;//定义临界区
implementation
{$R *.dfm}
var
tick: Integer = 1;
procedure TMyThread.Execute;
begin
EnterCriticalSection(CriticalSection);//进入临界区
try
Form1.Edit1.Text := IntToStr(tick);
Inc(tick);
Sleep(10);
finally
LeaveCriticalSection(CriticalSection); //离开临界区
end;
end;
constructor TMyThread.Create;
begin
inherited Create(False);
FreeOnTerminate := True;
end;
procedure TForm1.RzButton1Click(Sender : TObject);
var
index: Integer;
begin
for index := 0 to 15 do
TMyThread.Create;
end;
procedure TForm1.FormCreate(Sender : TObject);
begin
InitializeCriticalSection(CriticalSection); //初始化临界区
end;
procedure TForm1.FormDestroy(Sender : TObject);
begin
DeleteCriticalSection(CriticalSection); //删除临界区
end;
(2). 使用TcriticalSection 类
TcriticalSection 是在SyncObjs 单元中定义的类,要使用它需要先uses SyncObjs 。它对上
面的那些临界区操作API 函数进行了封装,简化并方便了在Delphi 中的使用。例如
TcriticalSection.Enter 其实是调用了TRTLCriticalSection.Enter 。
使用TcriticalSection 类和一般类差不多,首先实例化TcriticalSection 类。使用的时候只要
在主线程当中创建这个临界对象(注意一定要在需要同步的子线程之外建立这个对象)。
Tcriticalsection 类的构造函数比较简单,没有带参数。
TcriticalSection.Enter 等效于EnterCriticalSection( ) 。
TcriticalSection.TryEnter 等效于TryEnterCriticalSection( ) 。
TcriticalSection.Leave 等效于LeaveCriticalSection( ) 。
例:
//在主线程中定义
var criticalsection : TCriticalsection;
criticalsection := TCriticalsection.Create;
…
//在子线程中使用
criticalsection.Enter;
try
...
finally
criticalsection.Leave;
end;
警告:临界区只有在所有的线程都使用它来访问全局内存时才起作用,如果有线程直接调用
内存,而不通过临界区,也会造成同时访问的问题。
注意:临界区主要是为实现线程之间同步的,但是使用的时候要注意,一定要在使用临界区
同步的线程之外建立该临界区(一般在主线程中定义临界区并初始化临界区)。临界区是一
个进程里的所有线程同步的最好办法,它不是系统级的,只是进程级的,也就是说它可能利
用进程内的一些标志来保证该进程内的线程同步,据Richter 说是一个记数循环。临界区只
能在同一进程内使用。
2. Mutex 互斥
互斥是在序列化访问资源时使用操作系统内核对象的一种方式。我们首先设置一个互斥对
象,然后访问资源,最后释放互斥对象。在设置互斥时,如果另一个线程(或进程)试图设
置相同的互斥对象,该线程将会停下来,直到前一个线程(或进程)释放该互斥对象为止。
注意它可以由不同应用程序共享。互斥的效果非常类似于临界区,除了两个关键的区别:首
先,互斥可用于跨进程的线程同步。其次,互斥对象能被赋予一个字符串名字,并且通过引
用此名字创建现有内核对象的附加句柄。线程同步使用临界区,进程同步使用互斥。
当一个互斥对象不再被一个线程所拥有, 它就处于发信号状态。此时首先调用
WaitForSingleObject( ) 函数(实现WaitFor 功能的API 还有几个,这是最简单的一个)的线
程就成为该互斥对象的拥有者,将互斥对象设为不发信号状态。当线程调用ReleaseMutex( )
函数并传递一个互斥对象的句柄作为参数时,这种拥有关系就被解除,互斥对象重新进入发
信号状态。
提示:临界区和互斥的作用类似,都是用来进行同步的,但它们间有以下一点差别。临界区
只能在进程内使用,也就是说只能是进程内的线程间的同步;而互斥则还可用在进程之间的;
临界区随着进程的终止而终止,而互斥,如果你不用CloseHandle( ) 的话,在进程终止后
仍然在系统内存在,也就是说它是操作系统全局内核对象;临界区与互斥最大的区别是在性
能上,临界区在没有线程冲突时,要用10 ~ 15 个时间片,而互斥由于涉及到系统内核要用
400 ~ 600 个时间片;临界区不是内核对象,它不由操作系统的低级部件管理,而且不能使
用句柄来操纵,而互斥属于操作系统内核对象。
(1). 使用CreateMutex( ) API 函数
调用函数CreateMutex( ) 来创建一个互斥。下面是函数的声明:
function CreateMutex(lpMutexAttributes: PSecurityAttributes; bInitialOwner: BOOL; lpName:
PWideChar): THandle; stdcall;
lpMutexAttributes 参数为一个指向TsecurityAttributtes 记录的指针。此参数通常设为nil ,
表示默认的安全属性。bInitalOwner 参数表示创建互斥的线程是否要成为此互斥对象的初始
拥有者,当此参数为False 时,表示互斥对象没有拥有者。lpName 参数指定互斥对象的名
称,该名称是大小写区分的,设为nil 表示无命名,如果参数不是设为nil ,函数会搜索
是否有同名的互斥对象存在,如果有,函数就会返回同名互斥对象的句柄。否则,就新创建
一个互斥对象并返回其句柄。
当使用完互斥时,应当调用CloseHandle( ) 来关闭它。
WaitForSingleObject( ) 函数的使用:
在线程中使用WaitForSingleObject( ) 来防止其他线程进入同步区域的代码。第一个调用
WaitForSingleObject( ) 函数的线程会将事件对象(不限于互斥对象)设为无信号状态,其它线
程调用WaitForSingleObject( ) 函数时会检查事件对象是否处于发信号状态,这时状态处于
无信号状态,所以其它线程会挂起等待而不执行同步区域中的代码。当第一个线程执行完同
步代码后会释放事件对象,事件对象重新进入发信号状态并唤醒等待线程,其它线程会再次
将事件对象设为无信号状态,防止另外的线程执行同步代码。这就实现了线程同步。
此函数声明如下:
function WaitForSingleObject(hHandle : THandle; dwMilliseconds : DWORD): DWORD; stdcall;
这个函数可以使当前线程在dwMilliseconds 参数指定的时间内等待事件对象信号,直到
hHandle 参数指定的事件对象进入发信号状态为止。当一个事件对象不再被线程拥有时,它
就进入发信号状态。当一个进程要终止时,它就进入发信号状态。dwMilliseconds 参数设为
0 ,这意味着只检查hHandle 参数指定的事件对象是否处于发信号状态,而后立即返回该
信号状态。dwMilliseconds 参数设为INFINITE ,表示如果信号不出现将一直等下去。
WaitForSingleObject( ) 在一个指定时间(dwMilliseconds)内等待一个事件对象变为有信号,
在此时间内,若等待的事件对象一直是无信号的,则调用线程将处于挂起状态,否则继续执
行。超过此时间后,线程继续运行。
WaitForSingleObject( ) 函数返回值及含义:
WAIT_ABANDONED 指定的对象是一个事件对象,该对象没有被拥有线程在线程结束前释
放。此时就称事件对象被抛弃。互斥对象的所有权被同意授予调用该函数的线程。互斥对象
被设置成为无信号状态
WAIT_OBJECT_0 指定的对象处于发信号状态
WAIT_TIMEOUT 等待的时间已过,对象仍然是非发信号状态
WAIT_FAILED 语句出错
WaitForMultipleObjects( ) 函数的使用:
WaitForMultipleObjects( ) 与WaitForSingleObject( ) 类似,只是它要么等待指定列表(由
lpHandles 指定)中若干个互斥对象(由nCount 决定)都变为有信号,要么等待一个列表
(由lpHandles 指定)中的一个对象变为有信号(由bWaitAll 决定)。该函数声明如下:
function WaitForMultipleObjects(nCount: DWORD; lpHandles: PWOHandleArray; bWaitAll:
BOOL; dwMilliseconds: DWORD): DWORD; stdcall;
nCount 参数表示句柄的数量,最大值为MAXIMUM_WAIT_OBJECTS(64),lpHandles 参数
是指向句柄数组的指针,lpHandles 类型可以为(Event,Mutex,Process,Thread,Semaphore)
数组,bWaitAll 参数表示等待的类型,如果为True 则等待所有信号量有效再往下执行,设
为False 则当有其中一个信号量有效时就向下执行,dwMilliseconds 参数表示超时时间,超
时后向下继续执行。
注意: 除WaitForSingleObject( ) 和WaitForMultipleObjects( ) 外, 你还可以使用
MsgWaitForMultipleObjects( ) 函数。该函数的详细情况请看Win32 API 联机文档。
WaitForSingleObject( ) 不仅仅用于互斥,也用于信号量或事件,因此这里用词为“事件对象”
而非互斥对象。在互斥例中,可以用互斥对象代替事件对象,同样,在信号量例中,也能以
信号量对象代替事件对象。
再次提示,当一个互斥对象不再被一个线程所拥有,它就处于发信号状态。此时首先调用
WaitForSingleObject( ) 函数的线程就成为该互斥对象的拥有者,此互斥对象设为无信号状
态。当线程调用ReleaseMutex( ) 函数并传递一个互斥对象的句柄作为参数时,这种拥有关
系就被解除,互斥对象重新进入发信号状态。ReleaseMutex( ) 声明如下:
function ReleaseMutex(hMutex: THandle): BOOL; stdcall;
进程间需要同步时,只需要执行CreateMutex( ) 建立一个互斥对象,需要同步的时候只需
要WaitForSingleObject(mutexhandle, INFINITE) ,释放时只需要ReleaseMutex(mutexhandle)
即可。
例:
//先在主线程中创建互斥对象
var
hMutex : THandle = 0;//定义一个句柄
...
hMutex := CreateMutex(nil, False, nil);//创建互斥对象,并返回其句柄
//在子线程的Execute 方法中加入以下代码
WaitForSingleObject(hMutex, INFINITE);//互斥对象处于发信号状态时进入同步区,否则等待
...
ReleaseMutex(hMutex);
//最后记得要在主线程中释放互斥对象
CloseHandle(hMutex);//关闭句柄
(2). 使用TMutex 类
TMutex 是在SyncObjs 单元中定义的类,其是ThandleObject 类的子类,要使用它需要先
uses SyncObjs 。它对上面的那些互斥操作API 函数进行了封装,简化并方便了在Delphi
中的使用。
使用前先实例化TMutex 类,其有多个重载的构造函数。声明如下:
constructor Create(UseCOMWait: Boolean = False); overload;
constructor Create(MutexAttributes: PSecurityAttributes; InitialOwner: Boolean; const Name:
string; UseCOMWait: Boolean = False); overload;
constructor Create(DesiredAccess: LongWord; InheritHandle: Boolean; const Name: string;
UseCOMWait: Boolean = False); overload;
其实简单的直接调用TMutex.Create 就可以返回一个TMutex 对象。
第一个版本将创建一个无名的、使用默认安全属性、创建其的线程非互斥对象的初始拥有者
的TMutex 对象,其中的参数UseCOMWait 设为True 时表示当某个线程阻塞且等待互斥
对象时,任何单线程单元( STA ) COM 组件调用可以发回到该线程,其默认为False 。
第二个版本的MutexAttributes 参数通常设为nil 表示使用默认的安全属性。InitialOwner 参
数表示创建线程是否是互斥对象的初始拥有者。Name 参数表示互斥对象的名字,大小写区
分。
第三个版本的DesiredAccess 参数表示访问互斥的方式,如果传递的访问方式没有被允许那
么构造函数会失败,其参数可以是下面几个常量的任意组合:
MUTEX_ALL_ACCESS, MUTEX_MODIFY_STATE, SYNCHRONIZE, _DELETE,
READ_CONTROL , WRITE_DAC , WRITE_OWNER 。但任何组合必须包含
SYNCHRONIZE 访问权。InheritHandle 参数表示子进程是否可继承该互斥对象句柄。
TMutex.Acquire 等效于WaitForSingleObject(mutexhandle, INFINITE) ,其实际上就是执行
THandleObject.WaitFor(INFINITE)。
TMutex.Release 实际上就是执行ReleaseMutex(mutexhandle)。
TMutex.Acquire 只能无限期等待一个互斥对象,要设置等待时间或等待多个互斥对象要使
用TMutex.WaitFor( ) 或TMutex.WaitForMultiple( )。
WaitFor( ) 是定义在TMutex 的父类ThandleObject 中的虚函数,声明如下:
function WaitFor(Timeout: LongWord): TWaitResult; virtual;
其中返回值枚举型TWaitResult 可以指示操作结果,wrSignaled 代表信号已set ,
wrTimeOut 代表超时且信号未set ,wrAbandoned 代表超时前事件对象被销毁,wrError 代
表等待时出错。
WaitForMultiple( ) 是定义在TMutex 的父类ThandleObject 中的类函数,声明如下:
class function WaitForMultiple(const HandleObjs: THandleObjectArray; Timeout: LongWord;
AAll: Boolean; out SignaledObj: THandleObject; UseCOMWait: Boolean = False; Len: Integer =
0): TWaitResult;
其中HandleObjs 参数是包含了要等待的一系列事件对象的数组,AAll 参数设为True 时,
当所有事件对象都进入发信号状态后该函数调用才会完成,当返回值为wrSignaled 且
AAll 参数设为False 时,第一个发信号的事件对象会被传给SignaledObj 参数,Len 参数
设置监视事件对象的数量。
注意:WaitFor( ) 和WaitForMultiple( ) 均定义在ThandleObject 类中,而ThandleObject 类
是TMutex 、TSemaphore 、TEvent 类的父类,所以在描述WaitFor( ) 和WaitForMultiple( )
时使用的是事件对象而非互斥对象或信号量对象。
3. Semaphore 信号量
另一种使线程同步的技术是使用信号量对象。它是在互斥的基础上建立的,它与互斥相似,
但它可以计数。信号量增加了资源计数的功能,预定数目的线程允许同时进入要同步的代码。
例如可以允许一个给定资源同时被三个线程访问。其实互斥就是最大计数为1 的信号量。
信号量的使用和互斥差不多。
(1). 使用CreateSemaphore( ) API 函数
可以用CreateSemaphore( ) 来创建一个信号量对象,其声明如下:
function CreateSemaphore(lpSemaphoreAttributes: PSecurityAttributes;
lInitialCount, lMaximumCount: Longint; lpName: PWideChar): THandle; stdcall;
和CreateMutex( ) 函数一样, CreateSemaphore( ) 的第一个参数也是一个指向
TSecurityAttributes 记录的指针,此参数的缺省值可以设为nil 。
lInitialCount 参数用来指定一个信号量的初始计数值,这个值必须在0 和lMaximumCount
之间。此参数大于0 ,就表示信号量处于发信号状态。参数lMaximumCount 指定计数值
的最大值。如果这个信号量代表某种资源,那么这个值代表可用资源总数。
参数lpName 用于给出信号量对象的名称,它类似于CreateMutex( ) 函数的lpName 参数。
在程序中使用WaitForSingleObject( ) 来防止其他线程进入同步区域的代码。当调用
WaitForSingleObject( ) 函数( 或其他WaitFor 函数) 时, 此计数值就减1 。当调用
ReleaseSemaphore( ) 时,此计数值加1 ,此时同步区域代码可以被其它线程访问。其声明
如下:
function ReleaseSemaphore(hSemaphore: THandle; lReleaseCount: Longint;
lpPreviousCount: Pointer): BOOL; stdcall;
其中hSemaphore 参数是创建的信号量句柄,lReleaseCount 参数是释放时要增加的信号量
计数,lpPreviousCount 参数是通过该指针参数来获得释放前的信号量计数,如果不用设为
nil 。
当使用完信号量时,应当调用CloseHandle( ) 来关闭它。
注意:一般的同步使用互斥,是因为其有一个特别之处,当一个持有互斥的线程DOWN 掉
的时候,互斥可以自动让其它等待这个对象的线程接受,而其它的内核对象则不具体这个功
能。之所以要使用信号量则是因为其可以提供一个活动线程的上限,即lMaximumCount 参
数,这才是它的真正有用之处。
例:
var
Form1 : TForm1;
HSem : THandle = 0;//定义一个信号量
implementation
var
tick : Integer = 0;
procedure TMyThread.Execute;
var
WaitReturn : DWord ;
begin
WaitReturn := WaitForSingleObject(HSem, INFINITE);//使用信号量对象,信号量减1
Form1.Edit1.Text := IntToStr(tick);
Inc(tick);
Sleep(10);
ReleaseSemaphore(HSem, 1, Nil);//释放信号量对象,信号量加1
end;
…
procedure TForm1.FormCreate(Sender: TObject);
begin
HSem := CreateSemaphore(Nil, 1, 1, Nil);//创建信号量对象
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
CloseHandle(HSem);//销毁信号量
end;
procedure TForm1.Button1Click(Sender: TObject);
var
index : Integer;
begin
for index := 0 to 10 do
TMyThread.Create;
end;
(2). 使用TSemaphore 类
TSemaphore 是在SyncObjs 单元中定义的类,其是ThandleObject 类的子类,要使用它需
要先uses SyncObjs 。它对上面的API 函数进行了封装,简化并方便了在Delphi 中的使
用。
其有三个版本的构造器,简单执行TSemaphore.Create 就可实例化一个对象:
constructor Create(UseCOMWait: Boolean = False); overload;
constructor Create(SemaphoreAttributes: PSecurityAttributes; AInitialCount: Integer;
AMaximumCount: Integer; const Name: string; UseCOMWait: Boolean = False); overload;
constructor Create(DesiredAccess: LongWord; InheritHandle: Boolean;
const Name: string; UseCOMWait: Boolean = False); overload;
参数参见上面介绍。
TSemaphore.Acquire 等效于WaitForSingleObject(semaphorehandle, INFINITE) ,其实际上就
是执行THandleObject.WaitFor(INFINITE)。或者使用WaitFor( ) 和WaitForMultiple( ) 函数,
这两个函数可以设置等待的时间或等待多个事件对象。
TSemaphore.Release 有两个版本,声明如下:
procedure Release; override; overload;
function Release(AReleaseCount: Integer): Integer; overload; reintroduce;
第一个版本实际执行ReleaseSemaphore(FHandle, 1, nil)
第二个版本AReleaseCount 参数表示释放时增加的信号量计数值,返回值是释放前的信号
量计数值。实际执行ReleaseSemaphore(FHandle, AReleaseCount, @Result),其中@Result 是
指向Release 函数返回值Integer 类型的指针。如果要指定增加计数值应使用第二个版本。
4. Event 事件
事件( Event )与Delphi 中的事件有所不同。从本质上说,Event 其实相当于一个全局的布
尔变量。它有两个赋值操作: SetEvent 和ResetEvent ,相当于把它设置为True 或False 。
而检查它的值是通过WaitForSingleObject( ) (或其它WaitFor 函数)操作进行。SetEvent 和
ResetEvent 操作是原语操作,所以Event 可以实现一般布尔变量不能实现的在多线程中的
应用。
当Event 从Reset 状态向Set 状态转换时,唤醒其它挂起的线程,这就是它为什么叫
Event 的原因。所谓“事件”就是指“状态的转换”。通过Event 可以在线程间传递这种“状
态转换”信息。所以其本质是用来通知某事已经发生的信号,在这里可用来表示共享资源已
经在使用或已经使用完的信号。
(1). 使用CreateEvent( ) API 函数
使用CreateEvent( ) 创建一个事件,声明如下:
function CreateEvent(lpEventAttributes: PSecurityAttributes;
bManualReset, bInitialState: BOOL; lpName: PWideChar): THandle; stdcall;
其中bManualReset 参数代表创建的Event 是自动复位还是人工复位,如果设为True 表示
人工复位,一旦该Event 被设置为有信号,则它一直会等到手动执行ResetEvent( ) 时才会
变为无信号,设为False 表示自动复位,Event 被设置为有信号时,则当有一个线程执行
WaitForSingleObject( ) 时该Event 就会自动复位,变成无信号。bInitialState 参数代表事件
的初始状态,设为True,事件创建后为有信号,设为False 则为无信号。
不同于互斥或信号量,Event 不使用Release 相关函数设置相关对象进入发信号状态,而使
用SetEvent( ) 函数,当线程执行完同步代码要从同步区域中离开时应执行该函数,声明如
下:
function SetEvent(hEvent: THandle): BOOL; stdcall;
当事件创建为人工复位时,在线程进入同步区域执行同步代码前应执行ResetEvent( ) 函数,
将Event 设为无信号。声明如下:
function ResetEvent(hEvent: THandle): BOOL; stdcall;
PulseEvent( ) 是一个比较有意思的方法,正如名字,它使一个Event 对象的状态发生一次
脉冲变化,将无信号设为有信号,唤醒等待的线程,再设为无信号,而整个操作是原子的。
对自动复位的Event 对象,它仅唤醒第一个等到该事件的线程(如果有的话),而对于人工复
位的Event 对象,它唤醒所有等待的线程。声明如下:
function PulseEvent(hEvent: THandle): BOOL; stdcall;
当使用完事件时,应当调用CloseHandle( ) 来关闭它。
(2). 使用TEvent 类
TEvent 是在SyncObjs 单元中定义的类,其是ThandleObject 类的子类,要使用它需要先
uses SyncObjs 。它对上面的API 函数进行了封装,简化并方便了在Delphi 中的使用。
TEvent 若在多线程环境中可用于与其它线程同步;若在单线程环境中可用于调整响应不同
异步事件(如系统消息或用户动作)的代码段。构造函数如下:
constructor Create(EventAttributes: PSecurityAttributes; ManualReset: Boolean;
InitialState: Boolean; const Name: string; UseCOMWait: Boolean = False); overload;
constructor Create(UseCOMWait: Boolean = False); overload;
ManualReset 参数为是否手工复位,InitialState 参数为初始状态。
TEvent.SetEvent( ) 和TEvent.ResetEvent( ) 均无参数。
TEvent 类中没有定义与PulseEvent 功能一样的方法。
TEvent 类同样可以使用WaitFor( ) 和WaitForMultiple( ) 函数。
但要注意的是,TEvent 类并没有实现Acquire 函数,该函数是定义在TSynchroObject 类
中仅作为接口、没有执行代码的虚函数。TSynchroObject 是ThandleObject 类的父类。其实
自己实现Acquire 函数也不难,它实际上是执行THandleObject.WaitFor(INFINITE) 函数,
仿照上面的TMutex 类写就可以。
另外,Delphi 中定义了一个更简单的事件类,TSimpleEvent 类,但从源代码上看,该类仅
有TSimpleEvent = class(TEvent); 一句,并未定义任何属于TSimpleEvent 的成员。估计是
作为向后兼容而存在。
5. Global Atom 全局原子
Windows 系统中,为了实现信息共享,系统维护了一张全局原子表( Global Atom Table ),
用于保存字符串与之对应的标志符(原子)的组合,系统能保证其中的每个原子都是唯一的,
管理其引用计数,并且当该全局原子的引用计数为0 时,从内存中清除。应用程序在原子表
中可以放置字符串,并接收一个16 位整数值(叫做原子,即Atom ),它可以用来提取该字
符串。放在原子表中的字符串叫做原子的名字。系统提供了许多原子表。每个表有不同的目
的。例如,动态数据交换( DDE )应用程序使用全局原子表与其他应用程序共享项目名称和
主题名称字符串,不传递实际的字符串,一个DDE 应用程序传递全局原子给它的父进程,
父进程使用原子提取原子表中的字符串,这就是利用全局原子进行进程或线程间的数据交
换;使用全局原子也可防止多次启动某个程序。
应用程序可以使用本地原子表来有效地管理大量只用于程序内部的字符串。这些字符串,以
及相关联的原子,只对创建该原子表的应用程序可用。一个在许多数据结构中需要相同字符
串的应用程序,可以通过使用本地原子表来减少内存使用。程序可以把字符串放入原子表,
把相关的原子放入结构,而无需把字符串拷到每个结构中。这样,一个字符串在内存中只出
现一次,但可以在程序中多次使用。应用程序也可以使用本地原子表来快速搜索特定的字符
串。要实现这样的搜索,程序只需把要搜索的字符串放入原子表中,然后把结果原子与相关
数据结构中的原子相比较。通常情况下,比较原子要比比较字符串要快得多。原子表是用哈
希表实现的。默认时,一个本地原子表使用37 个bucket 的哈希表。不过,你可以通过调
用InitAtomTable 函数来改变bucket 数量。如果程序准备调用InitAtomTable ,那它必须
在调用任何其他原子管理函数前调用它。这里只简单介绍本地原子表。它有多个相关的函数,
function InitAtomTable(nSize: DWORD): BOOL; stdcall;
function DeleteAtom(nAtom: ATOM): ATOM; stdcall;
function AddAtom(lpString: PWideChar): ATOM; stdcall;
function FindAtom(lpString: PWideChar): ATOM; stdcall;
function GetAtomName(nAtom: ATOM; lpBuffer: PWideChar; nSize: Integer): UINT; stdcall;
以下介绍全局原子表相关函数。
function GlobalAddAtom(lpString: PWideChar): ATOM; stdcall;
增加一个字符串到全局原子表中,并返回一个唯一标识值。
lpString 参数为要添加到全局原子表中的字符串。
如果成功返回新增加的全局原子,失败则返回0 。ATOM 类型等于Word 类型。
function GlobalDeleteAtom(nAtom: ATOM): ATOM; stdcall;
减少对指定全局原子的引用计数,引用计数减1 ,如果引用计数为零,系统会在全局原子
表中删除此原子。
此函数一直返回0 。
只要全局原子的引用计数大于0 ,其原子名称将保留在全局原子表中,即使把它放入表中
的应用程序终结了。一个本地的原子表在应用程序终结时被销毁,而不管其中原子的引用计
数是多少。
function GlobalFindAtom(lpString: PWideChar): ATOM; stdcall;
在全局原子表中查找是否存在指定字符串。
lpString 参数为要查找的字符串。
如果在全局原子表中存在要查找的字符串,则返回此字符串对应的原子,没有找到则返回0。
function GlobalGetAtomName(nAtom: ATOM;
lpBuffer: PWideChar; nSize: Integer): UINT; stdcall;
返回指定原子所对应的字符串。
nAtom 参数为指定查找的原子,lpBuffer 参数为要存放字符串的缓冲区,nSize 参数为缓冲
区大小。
若操作成功返回缓冲区接受长度,若失败返回0 。UINT 类型等于LongWord 类型。
例:
//在程序的program 文件中
...
if GlobalFindAtom(iAtom) = 0 then
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end
else
MessageBox(0, ‘已经有一个程序在运行‘, ‘ ‘, mb_OK);
...
6. Synchronize 同步
Synchronize( ) 是定义在TThread 类中的函数,它可以让要执行的代码实现线程同步,但这
种同步其实是伪同步,其原理是将子线程要执行的代码通过消息传递给主线程,由主线程来
执行,主线程将代码放在一个隐蔽的窗口里运行,而子线程会等待主线程将执行结果发给它,
这样的话,这段代码就不是子线程代码,而是一般的主线程代码。Synchronize( ) 只是将该
线程的代码放到主线程中运行,并非实际意义的线程同步。RAD Studio VCL Reference 中也
描述为:Executes a method call within the main thread,Synchronize causes the call specified by
AMethod(参数) to be executed using the main thread,,thereby avoiding multi-thread conflicts。
这里有一个问题,如果Synchronize( ) 执行的代码很繁忙,例如执行的代码运算过于复杂、
庞大或者从数据库中取出大量数据,数据库不会立即返回数据时或者使用ADO 组件连接
数据库,而这时数据库无法连接,ADO 组件需要超时才会终止运行,这些都会导致主窗口
会阻塞掉,看似死机一般。因此,通常对用户界面类VCL 组件的访问才使用Synchronize( )
函数,一般用户界面类VCL 组件都由主线程创建、存在于主窗口中,而且对VCL 组件的
访问或修改的执行效率都比较高,不会过多的影响性能。绝对不能在主线程中执行
Synchronize( ) 函数,这会导致无限循环。
Synchronize( ) 函数一般在线程的Execute 函数中调用。其有四个版本,两个是类函数,两
个是静态函数,声明如下:
class procedure Synchronize(AThread: TThread; AMethod: TThreadMethod); overload;
class procedure Synchronize(AThread: TThread; AThreadProc: TThreadProcedure); overload;
procedure Synchronize(AMethod: TThreadMethod); overload;
procedure Synchronize(AThreadProc: TThreadProcedure); overload;
AThread 参数是当前线程,TThreadMethod 是对象的函数指针类型,TThreadProcedure 是匿
名函数类型。
注意:Synchronize( ) 的AMethod 或AThreadProc 参数必须是一个无参数的procedure ,
故在此procedure 中无法传递参数值,通常的解决方法是在线程类中增加额外的成员,用其
代替参数来传递信息。
例:
type
TMyThread = class(TThread)
str : string;//额外的域,代替参数将字符串写入Memo
...
procedure TMyThread.WriteMemo;
begin
Memo.Lines.Add(str);
end;
...
procedure TMyThread.Execute;
begin
str := ‘Hello‘;
synchronize(WriteMemo);
end;
上次跟大家分享了线程的标准代码,其实在线程的使用中最重要的是线程的同步问题,如果你在使用线程后,发现你的界面经常被卡死,或者无法显示出来,显示混乱,你的使用的变量值老是不按预想的变化,结果往往出乎意料,那么你很有可能是忽略了线程同步的问题。
当有多个线程的时候,经常需要去同步这些线程以访问同一个数据或资源。例如,假设有一个程序,其中一个线程用于把文件读到内存,而另一个线程用于统计文件中的字符数。当然,在把整个文件调入内存之前,统计它的计数是没有意义的。但是,由于每个操作都有自己的
线程,操作系统会把两个线程当作是互不相干的任务分别执行,这样就可能在没有把整个文
件装入内存时统计字数。为解决此问题,你必须使两个线程同步工作。存在一些线程同步地
址的问题,Windows 提供了许多线程同步的方式。在本节您将看到使用临界区、互斥、信
号量、事件、全局原子和Synchronize 函数来解决线程同步的问题。
下面的同步技术一般均有两种使用方式,一种是直接使用Windows API 函数,一种是使用
由Delphi 对API 函数进行封装的类。
以下函数以Delphi 2009 中的函数格式为准。
1. Critical Sections 临界区
临界区是一种最直接的线程同步方式。所谓临界区,就是一次只能由一个线程来执行的一段
代码。例如把初始化数组的代码放在临界区内,另一个线程在第一个线程处理完之前是不会
被执行的。临界区非常适合于序列化对一个进程中的数据的访问,因为它们的速度很快。
(1). 使用EnterCriticalSection( ) 和LeaveCriticalSection( ) API 函数
在使用临界区之前, 必须定义一个TRTLCriticalSection 类型的记录变量并使用
InitializeCriticalSection( ) 过程来初始化临界区。该过程多半在窗体创建时或在程序初始化时
执行。
其声明如下:
procedure InitializeCriticalSection(var lpCriticalSection : TRTLCriticalSection);stdcall;
lpCriticalSection 参数是一个TRTLCriticalSection 类型的记录, 并且是变参。至于
TRTLCriticalSection 是如何定义的,这并不重要,因为很少需要查看这个记录中的具体内容。
只需要在lpCriticalSection 中传递未初始化的记录, InitializeCriticalSection( ) 过程就会填
充这个记录。
注意:Microsoft 故意隐瞒了TRTLCriticalSection 的细节。因为,其内容在不同的硬件平台
2
上是不同的。在基于Intel 的平台上,TRTLCriticalSection 包含一个计数器、一个指示当前
线程句柄的域和一个系统事件的句柄。在Alpha 平台上,计数器被替换为一种Alpha-CPU数据结构,称为spinlock 。
在记录被填充后,我们就可以开始创建临界区了。这时我们需要用EnterCriticalSection( ) 和
LeaveCriticalSection( ) 来封装代码块,这两个函数分别代表进入和离开临界区,将要同步的
代码块放在这两个函数中间。在第一个线程调用了EnterCriticalSection( ) 之后,所有别的
线程就不能再进入代码块并挂起等待第一个线程离开临界区。下一个线程要等第一个线程调
用LeaveCriticalSection( ) 后才能被唤醒。这两个过程的声明如下:
procedure EnterCriticalSection(var lpCriticalSection : TRTLCriticalSection);stdcall; //进入临界区
procedure LeaveCriticalSection(var lpCriticalSection : TRTLCriticalSection);stdcall; //离开临界
区
正如你所想的,参数lpCriticalSection 就是由InitializeCriticalSection( ) 填充的记录。
如果在某个子线程执行EnterCriticalSection( ) 前,已经有另一个线程进入临界区且还未离
开临界区,则该子线程将挂起并无限期等待另一个线程离开临界区,要想不挂起且0 时间
等待,必须使用TryEnterCriticalSection( ) 。该过程声明如下:
function TryEnterCriticalSection(var lpCriticalSection: TRTLCriticalSection): BOOL; stdcall;
TryEnterCriticalSection( ) 不同于EnterCriticalSection( ) 的声明在于多出一个布尔型的返回
值,如果返回True 代表成功进入临界区,如果返回False 代表临界区已占用且不进入临界
区。运用这个函数,线程能够迅速查看它是否可以访问某个共享资源,如果不能访问,那么
它可以继续执行某些其他操作,而不必进行等待。
使用TryEnterCriticalSection( ) ,必须判断其返回值。
当你不需要临界区时,应当调用DeleteCriticalSection( ) 过程删除临界区,该函数多半在窗
体销毁时或程序终止前执行。下面是它的声明:
procedure DeleteCriticalSection(var lpCriticalSection : TRTLCriticalSection); stdcall;
例:
type
TMyThread = class(TThread)
protected
procedure Execute; override;
public
constructor Create; virtual;
end;
var
Form1 : TForm1;
CriticalSection : TRTLCriticalSection;//定义临界区
implementation
{$R *.dfm}
var
tick: Integer = 1;
3
procedure TMyThread.Execute;
begin
EnterCriticalSection(CriticalSection);//进入临界区
try
Form1.Edit1.Text := IntToStr(tick);
Inc(tick);
Sleep(10);
finally
LeaveCriticalSection(CriticalSection); //离开临界区
end;
end;
constructor TMyThread.Create;
begin
inherited Create(False);
FreeOnTerminate := True;
end;
procedure TForm1.RzButton1Click(Sender : TObject);
var
index: Integer;
begin
for index := 0 to 15 do
TMyThread.Create;
end;
procedure TForm1.FormCreate(Sender : TObject);
begin
InitializeCriticalSection(CriticalSection); //初始化临界区
end;
procedure TForm1.FormDestroy(Sender : TObject);
begin
DeleteCriticalSection(CriticalSection); //删除临界区
end;
(2). 使用TcriticalSection 类
TcriticalSection 是在SyncObjs 单元中定义的类,要使用它需要先uses SyncObjs 。它对上
面的那些临界区操作API 函数进行了封装,简化并方便了在Delphi 中的使用。例如
TcriticalSection.Enter 其实是调用了TRTLCriticalSection.Enter 。
使用TcriticalSection 类和一般类差不多,首先实例化TcriticalSection 类。使用的时候只要
在主线程当中创建这个临界对象(注意一定要在需要同步的子线程之外建立这个对象)。
Tcriticalsection 类的构造函数比较简单,没有带参数。
TcriticalSection.Enter 等效于EnterCriticalSection( ) 。
4
TcriticalSection.TryEnter 等效于TryEnterCriticalSection( ) 。
TcriticalSection.Leave 等效于LeaveCriticalSection( ) 。
例:
//在主线程中定义
var criticalsection : TCriticalsection;
criticalsection := TCriticalsection.Create;
…
//在子线程中使用
criticalsection.Enter;
try
...
finally
criticalsection.Leave;
end;
警告:临界区只有在所有的线程都使用它来访问全局内存时才起作用,如果有线程直接调用
内存,而不通过临界区,也会造成同时访问的问题。
注意:临界区主要是为实现线程之间同步的,但是使用的时候要注意,一定要在使用临界区
同步的线程之外建立该临界区(一般在主线程中定义临界区并初始化临界区)。临界区是一
个进程里的所有线程同步的最好办法,它不是系统级的,只是进程级的,也就是说它可能利
用进程内的一些标志来保证该进程内的线程同步,据Richter 说是一个记数循环。临界区只
能在同一进程内使用。
2. Mutex 互斥
互斥是在序列化访问资源时使用操作系统内核对象的一种方式。我们首先设置一个互斥对
象,然后访问资源,最后释放互斥对象。在设置互斥时,如果另一个线程(或进程)试图设
置相同的互斥对象,该线程将会停下来,直到前一个线程(或进程)释放该互斥对象为止。
注意它可以由不同应用程序共享。互斥的效果非常类似于临界区,除了两个关键的区别:首
先,互斥可用于跨进程的线程同步。其次,互斥对象能被赋予一个字符串名字,并且通过引
用此名字创建现有内核对象的附加句柄。线程同步使用临界区,进程同步使用互斥。
当一个互斥对象不再被一个线程所拥有, 它就处于发信号状态。此时首先调用
WaitForSingleObject( ) 函数(实现WaitFor 功能的API 还有几个,这是最简单的一个)的线
程就成为该互斥对象的拥有者,将互斥对象设为不发信号状态。当线程调用ReleaseMutex( )
函数并传递一个互斥对象的句柄作为参数时,这种拥有关系就被解除,互斥对象重新进入发
信号状态。
提示:临界区和互斥的作用类似,都是用来进行同步的,但它们间有以下一点差别。临界区
只能在进程内使用,也就是说只能是进程内的线程间的同步;而互斥则还可用在进程之间的;
临界区随着进程的终止而终止,而互斥,如果你不用CloseHandle( ) 的话,在进程终止后
仍然在系统内存在,也就是说它是操作系统全局内核对象;临界区与互斥最大的区别是在性
能上,临界区在没有线程冲突时,要用10 ~ 15 个时间片,而互斥由于涉及到系统内核要用
5
400 ~ 600 个时间片;临界区不是内核对象,它不由操作系统的低级部件管理,而且不能使
用句柄来操纵,而互斥属于操作系统内核对象。
(1). 使用CreateMutex( ) API 函数
调用函数CreateMutex( ) 来创建一个互斥。下面是函数的声明:
function CreateMutex(lpMutexAttributes: PSecurityAttributes; bInitialOwner: BOOL; lpName:
PWideChar): THandle; stdcall;
lpMutexAttributes 参数为一个指向TsecurityAttributtes 记录的指针。此参数通常设为nil ,
表示默认的安全属性。bInitalOwner 参数表示创建互斥的线程是否要成为此互斥对象的初始
拥有者,当此参数为False 时,表示互斥对象没有拥有者。lpName 参数指定互斥对象的名
称,该名称是大小写区分的,设为nil 表示无命名,如果参数不是设为nil ,函数会搜索
是否有同名的互斥对象存在,如果有,函数就会返回同名互斥对象的句柄。否则,就新创建
一个互斥对象并返回其句柄。
当使用完互斥时,应当调用CloseHandle( ) 来关闭它。
WaitForSingleObject( ) 函数的使用:
在线程中使用WaitForSingleObject( ) 来防止其他线程进入同步区域的代码。第一个调用
WaitForSingleObject( ) 函数的线程会将事件对象(不限于互斥对象)设为无信号状态,其它线
程调用WaitForSingleObject( ) 函数时会检查事件对象是否处于发信号状态,这时状态处于
无信号状态,所以其它线程会挂起等待而不执行同步区域中的代码。当第一个线程执行完同
步代码后会释放事件对象,事件对象重新进入发信号状态并唤醒等待线程,其它线程会再次
将事件对象设为无信号状态,防止另外的线程执行同步代码。这就实现了线程同步。
此函数声明如下:
function WaitForSingleObject(hHandle : THandle; dwMilliseconds : DWORD): DWORD; stdcall;
这个函数可以使当前线程在dwMilliseconds 参数指定的时间内等待事件对象信号,直到
hHandle 参数指定的事件对象进入发信号状态为止。当一个事件对象不再被线程拥有时,它
就进入发信号状态。当一个进程要终止时,它就进入发信号状态。dwMilliseconds 参数设为
0 ,这意味着只检查hHandle 参数指定的事件对象是否处于发信号状态,而后立即返回该
信号状态。dwMilliseconds 参数设为INFINITE ,表示如果信号不出现将一直等下去。
WaitForSingleObject( ) 在一个指定时间(dwMilliseconds)内等待一个事件对象变为有信号,
在此时间内,若等待的事件对象一直是无信号的,则调用线程将处于挂起状态,否则继续执
行。超过此时间后,线程继续运行。
WaitForSingleObject( ) 函数返回值及含义:
WAIT_ABANDONED 指定的对象是一个事件对象,该对象没有被拥有线程在线程结束前释
放。此时就称事件对象被抛弃。互斥对象的所有权被同意授予调用该函数的线程。互斥对象
被设置成为无信号状态
WAIT_OBJECT_0 指定的对象处于发信号状态
WAIT_TIMEOUT 等待的时间已过,对象仍然是非发信号状态
WAIT_FAILED 语句出错
WaitForMultipleObjects( ) 函数的使用:
WaitForMultipleObjects( ) 与WaitForSingleObject( ) 类似,只是它要么等待指定列表(由
lpHandles 指定)中若干个互斥对象(由nCount 决定)都变为有信号,要么等待一个列表
6
(由lpHandles 指定)中的一个对象变为有信号(由bWaitAll 决定)。该函数声明如下:
function WaitForMultipleObjects(nCount: DWORD; lpHandles: PWOHandleArray; bWaitAll:
BOOL; dwMilliseconds: DWORD): DWORD; stdcall;
nCount 参数表示句柄的数量,最大值为MAXIMUM_WAIT_OBJECTS(64),lpHandles 参数
是指向句柄数组的指针,lpHandles 类型可以为(Event,Mutex,Process,Thread,Semaphore)
数组,bWaitAll 参数表示等待的类型,如果为True 则等待所有信号量有效再往下执行,设
为False 则当有其中一个信号量有效时就向下执行,dwMilliseconds 参数表示超时时间,超
时后向下继续执行。
注意: 除WaitForSingleObject( ) 和WaitForMultipleObjects( ) 外, 你还可以使用
MsgWaitForMultipleObjects( ) 函数。该函数的详细情况请看Win32 API 联机文档。
WaitForSingleObject( ) 不仅仅用于互斥,也用于信号量或事件,因此这里用词为“事件对象”
而非互斥对象。在互斥例中,可以用互斥对象代替事件对象,同样,在信号量例中,也能以
信号量对象代替事件对象。
再次提示,当一个互斥对象不再被一个线程所拥有,它就处于发信号状态。此时首先调用
WaitForSingleObject( ) 函数的线程就成为该互斥对象的拥有者,此互斥对象设为无信号状
态。当线程调用ReleaseMutex( ) 函数并传递一个互斥对象的句柄作为参数时,这种拥有关
系就被解除,互斥对象重新进入发信号状态。ReleaseMutex( ) 声明如下:
function ReleaseMutex(hMutex: THandle): BOOL; stdcall;
进程间需要同步时,只需要执行CreateMutex( ) 建立一个互斥对象,需要同步的时候只需
要WaitForSingleObject(mutexhandle, INFINITE) ,释放时只需要ReleaseMutex(mutexhandle)
即可。
例:
//先在主线程中创建互斥对象
var
hMutex : THandle = 0;//定义一个句柄
...
hMutex := CreateMutex(nil, False, nil);//创建互斥对象,并返回其句柄
//在子线程的Execute 方法中加入以下代码
WaitForSingleObject(hMutex, INFINITE);//互斥对象处于发信号状态时进入同步区,否则等待
...
ReleaseMutex(hMutex);
//最后记得要在主线程中释放互斥对象
CloseHandle(hMutex);//关闭句柄
(2). 使用TMutex 类
TMutex 是在SyncObjs 单元中定义的类,其是ThandleObject 类的子类,要使用它需要先
uses SyncObjs 。它对上面的那些互斥操作API 函数进行了封装,简化并方便了在Delphi
中的使用。
使用前先实例化TMutex 类,其有多个重载的构造函数。声明如下:
7
constructor Create(UseCOMWait: Boolean = False); overload;
constructor Create(MutexAttributes: PSecurityAttributes; InitialOwner: Boolean; const Name:
string; UseCOMWait: Boolean = False); overload;
constructor Create(DesiredAccess: LongWord; InheritHandle: Boolean; const Name: string;
UseCOMWait: Boolean = False); overload;
其实简单的直接调用TMutex.Create 就可以返回一个TMutex 对象。
第一个版本将创建一个无名的、使用默认安全属性、创建其的线程非互斥对象的初始拥有者
的TMutex 对象,其中的参数UseCOMWait 设为True 时表示当某个线程阻塞且等待互斥
对象时,任何单线程单元( STA ) COM 组件调用可以发回到该线程,其默认为False 。
第二个版本的MutexAttributes 参数通常设为nil 表示使用默认的安全属性。InitialOwner 参
数表示创建线程是否是互斥对象的初始拥有者。Name 参数表示互斥对象的名字,大小写区
分。
第三个版本的DesiredAccess 参数表示访问互斥的方式,如果传递的访问方式没有被允许那
么构造函数会失败,其参数可以是下面几个常量的任意组合:
MUTEX_ALL_ACCESS, MUTEX_MODIFY_STATE, SYNCHRONIZE, _DELETE,
READ_CONTROL , WRITE_DAC , WRITE_OWNER 。但任何组合必须包含
SYNCHRONIZE 访问权。InheritHandle 参数表示子进程是否可继承该互斥对象句柄。
TMutex.Acquire 等效于WaitForSingleObject(mutexhandle, INFINITE) ,其实际上就是执行
THandleObject.WaitFor(INFINITE)。
TMutex.Release 实际上就是执行ReleaseMutex(mutexhandle)。
TMutex.Acquire 只能无限期等待一个互斥对象,要设置等待时间或等待多个互斥对象要使
用TMutex.WaitFor( ) 或TMutex.WaitForMultiple( )。
WaitFor( ) 是定义在TMutex 的父类ThandleObject 中的虚函数,声明如下:
function WaitFor(Timeout: LongWord): TWaitResult; virtual;
其中返回值枚举型TWaitResult 可以指示操作结果,wrSignaled 代表信号已set ,
wrTimeOut 代表超时且信号未set ,wrAbandoned 代表超时前事件对象被销毁,wrError 代
表等待时出错。
WaitForMultiple( ) 是定义在TMutex 的父类ThandleObject 中的类函数,声明如下:
class function WaitForMultiple(const HandleObjs: THandleObjectArray; Timeout: LongWord;
AAll: Boolean; out SignaledObj: THandleObject; UseCOMWait: Boolean = False; Len: Integer =
0): TWaitResult;
其中HandleObjs 参数是包含了要等待的一系列事件对象的数组,AAll 参数设为True 时,
当所有事件对象都进入发信号状态后该函数调用才会完成,当返回值为wrSignaled 且
AAll 参数设为False 时,第一个发信号的事件对象会被传给SignaledObj 参数,Len 参数
设置监视事件对象的数量。
注意:WaitFor( ) 和WaitForMultiple( ) 均定义在ThandleObject 类中,而ThandleObject 类
是TMutex 、TSemaphore 、TEvent 类的父类,所以在描述WaitFor( ) 和WaitForMultiple( )
时使用的是事件对象而非互斥对象或信号量对象。
3. Semaphore 信号量
另一种使线程同步的技术是使用信号量对象。它是在互斥的基础上建立的,它与互斥相似,
但它可以计数。信号量增加了资源计数的功能,预定数目的线程允许同时进入要同步的代码。
8
例如可以允许一个给定资源同时被三个线程访问。其实互斥就是最大计数为1 的信号量。
信号量的使用和互斥差不多。
(1). 使用CreateSemaphore( ) API 函数
可以用CreateSemaphore( ) 来创建一个信号量对象,其声明如下:
function CreateSemaphore(lpSemaphoreAttributes: PSecurityAttributes;
lInitialCount, lMaximumCount: Longint; lpName: PWideChar): THandle; stdcall;
和CreateMutex( ) 函数一样, CreateSemaphore( ) 的第一个参数也是一个指向
TSecurityAttributes 记录的指针,此参数的缺省值可以设为nil 。
lInitialCount 参数用来指定一个信号量的初始计数值,这个值必须在0 和lMaximumCount
之间。此参数大于0 ,就表示信号量处于发信号状态。参数lMaximumCount 指定计数值
的最大值。如果这个信号量代表某种资源,那么这个值代表可用资源总数。
参数lpName 用于给出信号量对象的名称,它类似于CreateMutex( ) 函数的lpName 参数。
在程序中使用WaitForSingleObject( ) 来防止其他线程进入同步区域的代码。当调用
WaitForSingleObject( ) 函数( 或其他WaitFor 函数) 时, 此计数值就减1 。当调用
ReleaseSemaphore( ) 时,此计数值加1 ,此时同步区域代码可以被其它线程访问。其声明
如下:
function ReleaseSemaphore(hSemaphore: THandle; lReleaseCount: Longint;
lpPreviousCount: Pointer): BOOL; stdcall;
其中hSemaphore 参数是创建的信号量句柄,lReleaseCount 参数是释放时要增加的信号量
计数,lpPreviousCount 参数是通过该指针参数来获得释放前的信号量计数,如果不用设为
nil 。
当使用完信号量时,应当调用CloseHandle( ) 来关闭它。
注意:一般的同步使用互斥,是因为其有一个特别之处,当一个持有互斥的线程DOWN 掉
的时候,互斥可以自动让其它等待这个对象的线程接受,而其它的内核对象则不具体这个功
能。之所以要使用信号量则是因为其可以提供一个活动线程的上限,即lMaximumCount 参
数,这才是它的真正有用之处。
例:
var
Form1 : TForm1;
HSem : THandle = 0;//定义一个信号量
implementation
var
tick : Integer = 0;
procedure TMyThread.Execute;
var
WaitReturn : DWord ;
begin
WaitReturn := WaitForSingleObject(HSem, INFINITE);//使用信号量对象,信号量减1
9
Form1.Edit1.Text := IntToStr(tick);
Inc(tick);
Sleep(10);
ReleaseSemaphore(HSem, 1, Nil);//释放信号量对象,信号量加1
end;
…
procedure TForm1.FormCreate(Sender: TObject);
begin
HSem := CreateSemaphore(Nil, 1, 1, Nil);//创建信号量对象
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
CloseHandle(HSem);//销毁信号量
end;
procedure TForm1.Button1Click(Sender: TObject);
var
index : Integer;
begin
for index := 0 to 10 do
TMyThread.Create;
end;
(2). 使用TSemaphore 类
TSemaphore 是在SyncObjs 单元中定义的类,其是ThandleObject 类的子类,要使用它需
要先uses SyncObjs 。它对上面的API 函数进行了封装,简化并方便了在Delphi 中的使
用。
其有三个版本的构造器,简单执行TSemaphore.Create 就可实例化一个对象:
constructor Create(UseCOMWait: Boolean = False); overload;
constructor Create(SemaphoreAttributes: PSecurityAttributes; AInitialCount: Integer;
AMaximumCount: Integer; const Name: string; UseCOMWait: Boolean = False); overload;
constructor Create(DesiredAccess: LongWord; InheritHandle: Boolean;
const Name: string; UseCOMWait: Boolean = False); overload;
参数参见上面介绍。
TSemaphore.Acquire 等效于WaitForSingleObject(semaphorehandle, INFINITE) ,其实际上就
是执行THandleObject.WaitFor(INFINITE)。或者使用WaitFor( ) 和WaitForMultiple( ) 函数,
这两个函数可以设置等待的时间或等待多个事件对象。
TSemaphore.Release 有两个版本,声明如下:
procedure Release; override; overload;
function Release(AReleaseCount: Integer): Integer; overload; reintroduce;
第一个版本实际执行ReleaseSemaphore(FHandle, 1, nil)
第二个版本AReleaseCount 参数表示释放时增加的信号量计数值,返回值是释放前的信号
量计数值。实际执行ReleaseSemaphore(FHandle, AReleaseCount, @Result),其中@Result 是
10
指向Release 函数返回值Integer 类型的指针。如果要指定增加计数值应使用第二个版本。
4. Event 事件
事件( Event )与Delphi 中的事件有所不同。从本质上说,Event 其实相当于一个全局的布
尔变量。它有两个赋值操作: SetEvent 和ResetEvent ,相当于把它设置为True 或False 。
而检查它的值是通过WaitForSingleObject( ) (或其它WaitFor 函数)操作进行。SetEvent 和
ResetEvent 操作是原语操作,所以Event 可以实现一般布尔变量不能实现的在多线程中的
应用。
当Event 从Reset 状态向Set 状态转换时,唤醒其它挂起的线程,这就是它为什么叫
Event 的原因。所谓“事件”就是指“状态的转换”。通过Event 可以在线程间传递这种“状
态转换”信息。所以其本质是用来通知某事已经发生的信号,在这里可用来表示共享资源已
经在使用或已经使用完的信号。
(1). 使用CreateEvent( ) API 函数
使用CreateEvent( ) 创建一个事件,声明如下:
function CreateEvent(lpEventAttributes: PSecurityAttributes;
bManualReset, bInitialState: BOOL; lpName: PWideChar): THandle; stdcall;
其中bManualReset 参数代表创建的Event 是自动复位还是人工复位,如果设为True 表示
人工复位,一旦该Event 被设置为有信号,则它一直会等到手动执行ResetEvent( ) 时才会
变为无信号,设为False 表示自动复位,Event 被设置为有信号时,则当有一个线程执行
WaitForSingleObject( ) 时该Event 就会自动复位,变成无信号。bInitialState 参数代表事件
的初始状态,设为True,事件创建后为有信号,设为False 则为无信号。
不同于互斥或信号量,Event 不使用Release 相关函数设置相关对象进入发信号状态,而使
用SetEvent( ) 函数,当线程执行完同步代码要从同步区域中离开时应执行该函数,声明如
下:
function SetEvent(hEvent: THandle): BOOL; stdcall;
当事件创建为人工复位时,在线程进入同步区域执行同步代码前应执行ResetEvent( ) 函数,
将Event 设为无信号。声明如下:
function ResetEvent(hEvent: THandle): BOOL; stdcall;
PulseEvent( ) 是一个比较有意思的方法,正如名字,它使一个Event 对象的状态发生一次
脉冲变化,将无信号设为有信号,唤醒等待的线程,再设为无信号,而整个操作是原子的。
对自动复位的Event 对象,它仅唤醒第一个等到该事件的线程(如果有的话),而对于人工复
位的Event 对象,它唤醒所有等待的线程。声明如下:
function PulseEvent(hEvent: THandle): BOOL; stdcall;
当使用完事件时,应当调用CloseHandle( ) 来关闭它。
(2). 使用TEvent 类
TEvent 是在SyncObjs 单元中定义的类,其是ThandleObject 类的子类,要使用它需要先
uses SyncObjs 。它对上面的API 函数进行了封装,简化并方便了在Delphi 中的使用。
TEvent 若在多线程环境中可用于与其它线程同步;若在单线程环境中可用于调整响应不同
异步事件(如系统消息或用户动作)的代码段。构造函数如下:
constructor Create(EventAttributes: PSecurityAttributes; ManualReset: Boolean;
InitialState: Boolean; const Name: string; UseCOMWait: Boolean = False); overload;
11
constructor Create(UseCOMWait: Boolean = False); overload;
ManualReset 参数为是否手工复位,InitialState 参数为初始状态。
TEvent.SetEvent( ) 和TEvent.ResetEvent( ) 均无参数。
TEvent 类中没有定义与PulseEvent 功能一样的方法。
TEvent 类同样可以使用WaitFor( ) 和WaitForMultiple( ) 函数。
但要注意的是,TEvent 类并没有实现Acquire 函数,该函数是定义在TSynchroObject 类
中仅作为接口、没有执行代码的虚函数。TSynchroObject 是ThandleObject 类的父类。其实
自己实现Acquire 函数也不难,它实际上是执行THandleObject.WaitFor(INFINITE) 函数,
仿照上面的TMutex 类写就可以。
另外,Delphi 中定义了一个更简单的事件类,TSimpleEvent 类,但从源代码上看,该类仅
有TSimpleEvent = class(TEvent); 一句,并未定义任何属于TSimpleEvent 的成员。估计是
作为向后兼容而存在。
5. Global Atom 全局原子
Windows 系统中,为了实现信息共享,系统维护了一张全局原子表( Global Atom Table ),
用于保存字符串与之对应的标志符(原子)的组合,系统能保证其中的每个原子都是唯一的,
管理其引用计数,并且当该全局原子的引用计数为0 时,从内存中清除。应用程序在原子表
中可以放置字符串,并接收一个16 位整数值(叫做原子,即Atom ),它可以用来提取该字
符串。放在原子表中的字符串叫做原子的名字。系统提供了许多原子表。每个表有不同的目
的。例如,动态数据交换( DDE )应用程序使用全局原子表与其他应用程序共享项目名称和
主题名称字符串,不传递实际的字符串,一个DDE 应用程序传递全局原子给它的父进程,
父进程使用原子提取原子表中的字符串,这就是利用全局原子进行进程或线程间的数据交
换;使用全局原子也可防止多次启动某个程序。
应用程序可以使用本地原子表来有效地管理大量只用于程序内部的字符串。这些字符串,以
及相关联的原子,只对创建该原子表的应用程序可用。一个在许多数据结构中需要相同字符
串的应用程序,可以通过使用本地原子表来减少内存使用。程序可以把字符串放入原子表,
把相关的原子放入结构,而无需把字符串拷到每个结构中。这样,一个字符串在内存中只出
现一次,但可以在程序中多次使用。应用程序也可以使用本地原子表来快速搜索特定的字符
串。要实现这样的搜索,程序只需把要搜索的字符串放入原子表中,然后把结果原子与相关
数据结构中的原子相比较。通常情况下,比较原子要比比较字符串要快得多。原子表是用哈
希表实现的。默认时,一个本地原子表使用37 个bucket 的哈希表。不过,你可以通过调
用InitAtomTable 函数来改变bucket 数量。如果程序准备调用InitAtomTable ,那它必须
在调用任何其他原子管理函数前调用它。这里只简单介绍本地原子表。它有多个相关的函数,
function InitAtomTable(nSize: DWORD): BOOL; stdcall;
function DeleteAtom(nAtom: ATOM): ATOM; stdcall;
function AddAtom(lpString: PWideChar): ATOM; stdcall;
function FindAtom(lpString: PWideChar): ATOM; stdcall;
function GetAtomName(nAtom: ATOM; lpBuffer: PWideChar; nSize: Integer): UINT; stdcall;
以下介绍全局原子表相关函数。
function GlobalAddAtom(lpString: PWideChar): ATOM; stdcall;
增加一个字符串到全局原子表中,并返回一个唯一标识值。
lpString 参数为要添加到全局原子表中的字符串。
如果成功返回新增加的全局原子,失败则返回0 。ATOM 类型等于Word 类型。
12
function GlobalDeleteAtom(nAtom: ATOM): ATOM; stdcall;
减少对指定全局原子的引用计数,引用计数减1 ,如果引用计数为零,系统会在全局原子
表中删除此原子。
此函数一直返回0 。
只要全局原子的引用计数大于0 ,其原子名称将保留在全局原子表中,即使把它放入表中
的应用程序终结了。一个本地的原子表在应用程序终结时被销毁,而不管其中原子的引用计
数是多少。
function GlobalFindAtom(lpString: PWideChar): ATOM; stdcall;
在全局原子表中查找是否存在指定字符串。
lpString 参数为要查找的字符串。
如果在全局原子表中存在要查找的字符串,则返回此字符串对应的原子,没有找到则返回0。
function GlobalGetAtomName(nAtom: ATOM;
lpBuffer: PWideChar; nSize: Integer): UINT; stdcall;
返回指定原子所对应的字符串。
nAtom 参数为指定查找的原子,lpBuffer 参数为要存放字符串的缓冲区,nSize 参数为缓冲
区大小。
若操作成功返回缓冲区接受长度,若失败返回0 。UINT 类型等于LongWord 类型。
例:
//在程序的program 文件中
...
if GlobalFindAtom(iAtom) = 0 then
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end
else
MessageBox(0, ‘已经有一个程序在运行‘, ‘ ‘, mb_OK);
...
6. Synchronize 同步
Synchronize( ) 是定义在TThread 类中的函数,它可以让要执行的代码实现线程同步,但这
种同步其实是伪同步,其原理是将子线程要执行的代码通过消息传递给主线程,由主线程来
执行,主线程将代码放在一个隐蔽的窗口里运行,而子线程会等待主线程将执行结果发给它,
这样的话,这段代码就不是子线程代码,而是一般的主线程代码。Synchronize( ) 只是将该
线程的代码放到主线程中运行,并非实际意义的线程同步。RAD Studio VCL Reference 中也
描述为:Executes a method call within the main thread,Synchronize causes the call specified by
AMethod(参数) to be executed using the main thread,,thereby avoiding multi-thread conflicts。
这里有一个问题,如果Synchronize( ) 执行的代码很繁忙,例如执行的代码运算过于复杂、
庞大或者从数据库中取出大量数据,数据库不会立即返回数据时或者使用ADO 组件连接
数据库,而这时数据库无法连接,ADO 组件需要超时才会终止运行,这些都会导致主窗口
会阻塞掉,看似死机一般。因此,通常对用户界面类VCL 组件的访问才使用Synchronize( )
13
函数,一般用户界面类VCL 组件都由主线程创建、存在于主窗口中,而且对VCL 组件的
访问或修改的执行效率都比较高,不会过多的影响性能。绝对不能在主线程中执行
Synchronize( ) 函数,这会导致无限循环。
Synchronize( ) 函数一般在线程的Execute 函数中调用。其有四个版本,两个是类函数,两
个是静态函数,声明如下:
class procedure Synchronize(AThread: TThread; AMethod: TThreadMethod); overload;
class procedure Synchronize(AThread: TThread; AThreadProc: TThreadProcedure); overload;
procedure Synchronize(AMethod: TThreadMethod); overload;
procedure Synchronize(AThreadProc: TThreadProcedure); overload;
AThread 参数是当前线程,TThreadMethod 是对象的函数指针类型,TThreadProcedure 是匿
名函数类型。
注意:Synchronize( ) 的AMethod 或AThreadProc 参数必须是一个无参数的procedure ,
故在此procedure 中无法传递参数值,通常的解决方法是在线程类中增加额外的成员,用其
代替参数来传递信息。
例:
type
TMyThread = class(TThread)
str : string;//额外的域,代替参数将字符串写入Memo
...
procedure TMyThread.WriteMemo;
begin
Memo.Lines.Add(str);
end;
...
procedure TMyThread.Execute;
begin
str := ‘Hello‘;
synchronize(WriteMemo);
end;
内容页面 底部 百度漂浮
delphi线程同步