Delphi关于多线程同步的一些方法(转)
- 格式:pdf
- 大小:176.12 KB
- 文档页数:5
Delphi关于多线程同步的⼀些⽅法(转)
线程是进程内⼀个相对独⽴的、可调度的执⾏单元。
⼀个应⽤可以有⼀个主线程,⼀个主线程可以有多个⼦线程,⼦线程还可以有⾃⼰的⼦线程,这样就构成了多线程应⽤了。
由于多个线程往往会同时访问同⼀块内存区域,频繁的访问这块区域,将会增加产⽣线程冲突的概率。
⼀旦产⽣了冲突,将会造成不可预料的结果(该公⽤区域的值是不可预料的)可见处理线程同步的必要性。
注意:本⽂中出现的所有代码都是⽤DELPHI描述的,调试环境为Windows me ,Delphi 6。
其中所涉及的Windows API函数可以从MSDN获得详细的。
⾸先引⽤⼀个实例来引出我们以下的讨论,该实例没有采取任何措施来避免线程冲突,它的主要过程为:由主线程启动两个线程对letters这个全局变量进⾏频繁的读写,然后分别把修改的结果显⽰到ListBox中。
由于没有同步这两个线程,使得线程在修改letters时产⽣了不可预料的结果。
ListBox中的每⼀⾏的字母都应该⼀致,但是上图画线处则不同,这就是线程冲突产⽣的结果。
当两个线程同时访问该共享内存时,⼀个线程还未对该内存修改完,另⼀个线程⼜对该内存进⾏了修改,由于写值的过程没有被串⾏化,这样就产⽣了⽆效的结果。
可见线程同步的重要性。
以下是本例的代码
unit.pas⽂件
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
//定义窗⼝类
type
TForm1 = class(TForm)
ListBox1: TListBox;
ListBox2: TListBox;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
//定义线程类
type
TListThread=class(TThread)
private
Str:String;
protected
procedure AddToList;//将Str加⼊ListBox
Procedure Execute;override;
public
LBox:TListBox;
end;
//定义变量
var
Form1: TForm1;
Letters:String='AAAAAAAAAAAAAAAAAAAA';//全局变量
implementation
{$R *.dfm}
//线程类实现部分
procedure TListThread.Execute;
var
I,J,K:Integer;
begin
for i:=0 to 50 do
begin
for J:=1 to 20 do
for K:=1 to 1000 do//循环1000次增加产⽣冲突的⼏率
if letters[j]<'Z' then
letters[j]:=succ(Letters[j])
else
letters[j]:='A';
str:=letters;
synchronize(addtolist);//同步访问VCL可视
end;
end;
procedure TListThread.AddToList;
begin
LBox.Items.Add(str);//将str加⼊列表框
end;
//窗⼝类实现部分
procedure TForm1.Button1Click(Sender: TObject);
var
th1,th2:TListThread;
begin
Listbox1.Clear;
Listbox2.Clear;
th1:=tlistThread.Create(true);//创建线程1
th2:=tlistThread.Create(true);//创建线程2
th1.LBox:=listBox1;
th2.LBox:=listBox2;
th1.Resume;//开始执⾏
th2.Resume;
end;
end.
由上例可见,当多个线程同时修改⼀个公⽤变量时,会产⽣冲突,所以我们要设法防⽌它,这样我们开发的多线程应⽤才能够稳定地运⾏。
下⾯我们来改进它。
我们先使⽤临界段来串⾏化,实现同步。
在上例unit1.pas代码的uses段中加⼊SyncObjs单元,加⼊全局临界段变量(TRTLCriticalSection)Critical1,在FormCreate事件中加⼊InitializeCriticalSection(Critical1)这句代码,在FormDestroy事件中加⼊DeleteCriticalSection(Critical1)这句代码,然后修改TListThread.Execute函数,修改后的代码似如下所⽰(?处为增加的代码):
procedure TListThread.Execute;
var
I,J,K:Integer;
begin
for i:=0 to 50 do
begin
?EnterCriticalSection(Critical1);//进⼊临界段
for J:=1 to 20 do
for K:=1 to 3000 do
if letters[j]<'Z' then
letters[j]:=succ(Letters[j])
else
letters[j]:='A';
str:=letters;
?LeaveCriticalSection(Critical1);//退出临界段
synchronize(addtolist);
end;
end;
好了,重新编译,运⾏结果如下图所⽰(略)
成功的避免了冲突,看来真的很简单,我们成功了!当然我们还可以使⽤其它同步技术如Mutex(互斥对象), Semaphore(信号量)等,这些技术都是Windows通过API直接提供给我们的。
下⾯总结⼀下Windows常⽤的⼏种线程同步技术。
1. Critical Sections(临界段),源代码中如果有不能由两个或两个以上线程同时执⾏的部分,可以⽤临界段来使这部分的代码执⾏串⾏化。
它只能在⼀个独⽴的进程或⼀个独⽴的应⽤中使⽤。
使⽤⽅法如下:
//在窗体创建中
InitializeCriticalSection(Critical1)
//在窗体销毁中
DeleteCriticalSection(Critical1)
//在线程中
EnterCriticalSection(Critical1)
……保护的代码
LeaveCriticalSection(Critical1)
2. Mutex(互斥对象),是⽤于串⾏化访问资源的全局对象。
我们⾸先设置互斥对象,然后访问资源,最后释放互斥对象。
在设置互斥对象时,如果另⼀个线程(或进程)试图设置相同的互斥对象,该线程将会停下来,直到前⼀个线程(或进程)释放该互斥对象为⽌。
注意它可以由不同应⽤共享。
使⽤⽅法如下:
//在窗体创建中
hMutex:=CreateMutex(nil,false,nil)
//在窗体销毁中
CloseHandle(hMutex)
//在线程中
WaitForSingleObject(hMutex,INFINITE)
……保护的代码
ReleaseMutex(hMutex)
3. Semaphore(信号量),它与互斥对象相似,但它可以计数。
例如可以允许⼀个给定资源同时同时被三个线程访问。
其实Mutex就是最⼤计数为⼀的Semaphore。
使⽤⽅法如下:
//在窗体创建中
hSemaphore:= CreateSemaphore(nil,lInitialCount,lMaximumCount,lpName)
//在窗体销毁中
CloseHandle(hSemaphore)
//在线程中
WaitForSingleObject(hSemaphore,INFINITE)
……保护的代码
ReleaseSemaphore(hSemaphore, lReleaseCount, lpPreviousCount)
4. 还可以使⽤Delphi中的TcriticalSection这个VCL对象,它的定义在Syncobjs.pas中。
当你开发多线程应⽤时,并且多个线程同时访问⼀个共享资源或数据时,你需要考虑线程同步的问题了。
delphi中多线程同步的⼀些⽅法
[ 2006-01-09 10:48:03 | 作者: snox 字体⼤⼩:⼤ |中 |⼩ ]
当有多个线程的时候,经常需要去同步这些线程以访问同⼀个数据或资源。
例如,假设有⼀个,其中⼀个线程⽤于把⽂件读到内存,⽽另⼀个线程⽤于统计⽂件中的字符数。
当然,在把整个⽂件调⼊内存之前,统计它的计数是没有意义的。
但是,由于每个操作都有⾃⼰的线程,操作系统会把两个线程当作是互不相⼲的任务分别执⾏,这样就可能在没有把整个⽂件装⼊内存时统计字数。
为解决此问题,你必须使两个线程同步⼯作。
存在⼀些线程同步地址的问题,Win32提供了许多线程同步的⽅式。
在本节你将看到使⽤临界区、互斥、信号量和事件来解决线程同步的问题。
1. 临界区
临界区是⼀种最直接的线程同步⽅式。
所谓临界区,就是⼀次只能由⼀个线程来执⾏的⼀段代码。
如果把初始化数组的代码放在临界区内,另⼀个线程在第⼀个线程处理完之前是不会被执⾏的。
在使⽤临界区之前,必须使⽤InitializeCriticalSection()过程来初始化它。
其声明如下:
procedure InitializeCriticalSection(var
lpCriticalSection参数是⼀个TRTLCriticalSection类型的记录,并且是变参。
⾄于TRTLCriticalSection 是如何定义的,这并不重要,因为很少需要查看这个记录中的具体内容。
只需要在lpCriticalSection中传递未初始化的记录,InitializeCriticalSection()过程就会填充这个记录。
注意Microsoft故意隐瞒了TRTLCriticalSection的细节。
因为,其内容在不同的硬件平台上是不同的。
在基于Intel的平台
上,TRTLCriticalSection包含⼀个计数器、⼀个指⽰当前线程句柄的域和⼀个系统事件的句柄。
在Alpha平台上,计数器被替换为⼀种Alpha-CPU 数据结构,称为spinlock。
在记录被填充后,我们就可以开始创建临界区了。
这时我们需要⽤EnterCriticalSection()和LeaveCriticalSection()来封装代码块。
这两个过程的声明如下:
procedure EnterCriticalSection(var lpCriticalSection:TRRLCriticalSection);stdcall;
procedure LeaveCriticalSection(var
正如你所想的,参数lpCriticalSection就是由InitializeCriticalSection()填充的记录。
当你不需要TRTLCriticalSection记录时,应当调⽤DeleteCriticalSection()过程,下⾯是它的声明:
procedure DeleteCriticalSection(var
2. 互斥
互斥⾮常类似于临界区,除了两个关键的区别:⾸先,互斥可⽤于跨进程的线程同步。
其次,互斥能被赋予⼀个字符串名字,并且通过引⽤此名字创建现有互斥对象的附加句柄。
提⽰临界区与事件对象(⽐如互斥对象)的最⼤的区别是在性能上。
临界区在没有线程冲突时,要⽤1 0 ~ 1 5个时间⽚,⽽事件对象由于涉及到系统内核要⽤400~600个时间⽚。
可以调⽤函数CreateMutex ( )来创建⼀个互斥量。
下⾯是函数的声明:
function
lpMutexAttributes参数为⼀个指向TSecurityAttributtes记录的指针。
此参数通常设为0,表⽰默认的安全属性。
bInitalOwner参数表⽰创建互斥对象的线程是否要成为此互斥对象的拥有者。
当此参数为False时,表⽰互斥对象没有拥有者。
lpName参数指定互斥对象的名称。
设为nil表⽰⽆命名,如果参数不是设为nil,函数会搜索是否有同名的互斥对象存在。
如果有,函数就会返回同名互斥对象的句柄。
否则,就新创建⼀个互斥对象并返回其句柄。
当使⽤完互斥对象时,应当调⽤CloseHandle()来关闭它。
在中使⽤WaitForSingleObject()来防⽌其他线程进⼊同步区域的代码。
此函数声明如下:
function
这个函数可以使当前线程在dwMilliseconds指定的时间内睡眠,直到hHandle参数指定的对象进⼊发信号状态为⽌。
⼀个互斥对象不再被线程拥有时,它就进⼊发信号状态。
当⼀个进程要终⽌时,它就进⼊发信号状态。
dwMilliseconds参数可以设为0,这意味着只检查hHandle参数指定的对象是否处于发信号状态,⽽后⽴即返回。
dwMilliseconds参数设为INFINITE,表⽰如果信号不出现将⼀直等下去。
这个函数的返回值如下
WaitFor SingleObject()函数使⽤的返回值
返回值含义
WAIT_ABANDONED 指定的对象是互斥对象,并且拥有这个互斥对象的线程在没有释放此对象之前就已终⽌。
此时就称互斥对象被抛弃。
这种情况下,这个互斥对象归当前线程所有,并把它设为⾮发信号状态
WAIT_OBJECT_0 指定的对象处于发信号状态
WAIT_TIMEOUT等待的时间已过,对象仍然是⾮发信号状态再次声明,当⼀个互斥对象不再被⼀个线程所拥有,它就处于发信号状态。
此时⾸先调⽤WaitForSingleObject()函数的线程就成为该互斥对象的拥有者,此互斥对象设为不发信号状态。
当线程调⽤ReleaseMutex()函数并传递⼀个互斥对象的句柄作为参数时,这种拥有关系就被解除,互斥对象重新进⼊发信号状态。
注意除WaitForSingleObject()函数外,你还可以使⽤WaitForMultipleObject()和MsgWaitForMultipleObject()函数,它们可以等待⼏个对象变为发信号状态。
这两个函数的详细情况请看Win32 API联机。
3. 信号量
另⼀种使线程同步的技术是使⽤信号量对象。
它是在互斥的基础上建⽴的,但信号量增加了资源计数的功能,预定数⽬的线程允许同时进⼊要同步的代码。
可以⽤CreateSemaphore()来创建⼀个信号量对象,其声明如下:
function
和CreateMutex()函数⼀样,CreateSemaphore()的第⼀个参数也是⼀个指向TSecurityAttribute s记录的指针,此参数的缺省值可以设为nil。
lInitialCount参数⽤来指定⼀个信号量的初始计数值,这个值必须在0和lMaximumCount之间。
此参数⼤于0,就表⽰信号量处于发信号状态。
当调⽤WaitForSingleObject()函数(或其他函数)时,此计数值就减1。
当调⽤ReleaseSemaphore()时,此计数值加1。
参数lMaximumCount指定计数值的最⼤值。
如果这个信号量代表某种资源,那么这个值代表可⽤资源总数。
参数lpName⽤于给出信号量对象的名称,它类似于CreateMutex()函数的lpName参数。
——————————————————————————————————————————
★★★关于线程同步:
Synchronize()是在⼀个隐蔽的窗⼝⾥运⾏,如果在这⾥你的任务很繁忙,你的主窗⼝会阻塞掉;Synchronize()只是将该线程的代码放到主线程中运⾏,并⾮线程同步。
临界区是⼀个进程⾥的所有线程同步的最好办法,他不是系统级的,只是进程级的,也就是说他可能利⽤进程内的⼀些标志来保证该进程内的线程同步,据Richter说是⼀个记数循环;临界区只能在同⼀进程内使⽤;临界区只能⽆限期等待,不过2k增加了TryEnterCriticalSection 函数实现0时间等待。
互斥则是保证多进程间的线程同步,他是利⽤系统内核对象来保证同步的。
由于系统内核对象可以是有名字的,因此多个进程间可以利⽤这个有名字的内核对象保证系统资源的线程安全性。
互斥量是Win32 内核对象,由操作系统负责管理;互斥量可以使⽤WaitForSingleObject实现⽆限等待,0时间等待和任意时间等待。
1. 临界区
临界区是⼀种最直接的线程同步⽅式。
所谓临界区,就是⼀次只能由⼀个线程来执⾏的⼀段代码。
如果把初始化数组的代码放在临界区内,另⼀个线程在第⼀个线程处理完之前是不会被执⾏的。
在使⽤临界区之前,必须使⽤InitializeCriticalSection()过程来初始化它。
在第⼀个线程调⽤了EnterCriticalSection()之后,所有别的线程就不能再进⼊代码块。
下⼀个线程要等第⼀个线程调⽤LeaveCriticalSection()后才能被唤醒。
2. 互斥
互斥⾮常类似于临界区,除了两个关键的区别:⾸先,互斥可⽤于跨进程的线程同步。
其次,互斥能被赋予⼀个字符串名字,并且通过引⽤此名字创建现有互斥对象的附加句柄。
提⽰:临界区与事件对象(⽐如互斥对象)的最⼤的区别是在性能上。
临界区在没有线程冲突时,要⽤10 ~ 15个时间⽚,⽽事件对象由于涉及到系统内核要⽤400~600个时间⽚。
当⼀个互斥对象不再被⼀个线程所拥有,它就处于发信号状态。
此时⾸先调⽤WaitForSingleObject()函数的线程就成为该互斥对象的拥有者,此互斥对象设为不发信号状态。
当线程调⽤ReleaseMutex()函数并传递⼀个互斥对象的句柄作为参数时,这种拥有关系就被解除,互斥对象重新进⼊发信号状态。
可以调⽤函数CreateMutex()来创建⼀个互斥量。
当使⽤完互斥对象时,应当调⽤CloseHandle()来关闭它。
3. 信号量
另⼀种使线程同步的技术是使⽤信号量对象。
它是在互斥的基础上建⽴的,但信号量增加了资源计数的功能,预定数⽬的线程允许同时进⼊要同步的代码。
可以⽤CreateSemaphore()来创建⼀个信号量对象,
因为只允许⼀个线程进⼊要同步的代码,所以信号量的最⼤计数值(lMaximumCount)要设为1。
ReleaseSemaphore()函数将使信号量对象的计数加1;
记住,最后⼀定要调⽤CloseHandle()函数来释放由CreateSemaphore()创建的信号量对象的句柄。
★★★WaitForSingleObject函数的返值:
WAIT_ABANDONED指定的对象是互斥对象,并且拥有这个互斥对象的线程在没有释放此对象之前就已终⽌。
此时就称互斥对象被抛弃。
这种情况下,这个互斥对象归当前线程所有,并把它设为⾮发信号状态;
WAIT_OBJECT_0 指定的对象处于发信号状态;
WAIT_TIMEOUT等待的时间已过,对象仍然是⾮发信号状态;
——————————————————————————————————————————————
VCL⽀持三种技术来达到这个⽬的:
(2)使⽤critical区
如果对象没有提⾼内置的锁定功能,需要使⽤critical区,Critical区在同⼀个时间只也许⼀个线程进⼊。
为了使⽤Critical区,产⽣⼀个TCriticalSection全局的实例。
TcriticalSection有两个⽅法,Acquire(阻⽌其他线程执⾏该区域)和Release(取消阻⽌)
每个Critical区是与你想要保护的全局内存相关联。
每个访问全局内存的线程必须⾸先使⽤Acquire来保证没有其他线程使⽤它。
完成以后,线程调⽤Release⽅法,让其他线程也可以通过调⽤Acquire来使⽤这块全局内存。
警告:Critical区只有在所有的线程都使⽤它来访问全局内存,如果有线程直接调⽤内存,⽽不通过Acquire,会造成同时访问的问题。
例如:LockXY是⼀个全局的Critical区变量。
任何⼀个访问全局X, Y的变量的线程,在访问前,都必须使⽤Acquire
LockXY.Acquire;{ lock out other threads }
try
Y := sin(X);
finally
LockXY.Release;
end
临界区主要是为实现线程之间同步的,但是使⽤的时候注意,⼀定要在⽤此临界对象同步的线程之外建⽴该对象(⼀般在主线程中建⽴临界对象)。
————————————————————————————————————————————————
线程同步使⽤临界区,进程同步使⽤互斥对象。
Delphi中封装了临界对象。
对象名为TCriticalSection,使⽤的时候只要在主线程当中建⽴这个临界对象(注意⼀定要在需要同步的线程之外建⽴这个对象)。
具体同步的时候使⽤Lock和Unlock即可。
⽽进程间同步建⽴互斥对象,则只需要建⽴⼀个互斥对象CreateMutex. 需要同步的时候只需要WaitForSingleObject(mutexhandle, INFINITE) unlock的时候只需要ReleaseMutex(mutexhandle);即可。
有很多⽅法, 信号灯, 临界区, 互斥对象,此外, windows下还可以⽤全局原⼦,共享内存等等. 在windows体系中, 读写⼀个8位整数时原⼦的, 你可以依靠这⼀点完成互斥的⽅法. 对于能够产⽣全局名称的⽅法能够可以在进程间同步上(如互斥对象), 也可以⽤在线程间同步上;不能够产⽣全局名称的⽅法(如临界区)只能⽤在线程间同步上.。