现在的位置: 首页 > 综合 > 正文

WIN32下DELPHI中的多线程【同步2】(五)

2013年08月16日 ⁄ 综合 ⁄ 共 6033字 ⁄ 字号 评论关闭

线程同步2
      上一文中曾经介绍了线程同步的一些方法,其实完成同步还有很多很多的办法,这里最后介绍一种方式--信号量内核对象。并借此来回顾线程同步。
     在谈论信号量之前,我想先谈论另外一种方式,一种你最好不要使用的方式。假设你有一个公共内存区域,你不希望一个线程在完成一个操作之前另外一个线程对他进行另外的操作。抛开前面所有的知识,我们可以使用这样一种办法,一种所有人都会想到的办法。
      程序中设置一个布尔类型的公共变量FLAG,此公共变量唯一的最用是决定线程是否是否可以操作公共内存区域。如果是TRUE则允许操作,如果是FALSE则禁止操作。在线程将要执行对共享内存的操作时,反复判断此变量,类似一个死循环,直到FLAG变为TRUE。思路很简单,实现起来也比前面介绍的那些方法更容易,在某种意义上说,它也是有效的。但文章前面曾经说过,最好不用使用这种方式,为什么?回顾线程的工作状态,我们基本可以这样划分,
1、处于可调度状态(挂起),此状态下的线程正在等待CPU分配时间片给它来执行自己的操作
2、等待状态,此时的线程我们可以称它处在不可调度状态,CPU绝不会在等待事件未发生之前分配时间片给它,例如一个线程正在等待某件事情的发生,就比如前边说的等待事件内核对象的状态变为已通知
3、CPU已分配时间片给线程,它正在执行自己的操作。
      假如我们使用事件内核对象来完成一些线程的同步,那么前面曾经说过,当等待函数检测到事件内核对象的状态为未通知状态时,此线程将处于等待状态,此时线程不会使用CPU,而如果使用前面介绍的那种反复判断变量的方法,那么此线程将占用CPU资源,这很重要,我始终认为,对于一个合格的程序员而言,绝对不要无谓的浪费客户的CPU资源。
      虽然我说上面那种循环判断公共状态位的办法不可取,但它却反映了线程同步的思想,即使我们调用那些用于同步的API函数,事实上,同步的思想也是如此,只是实现的方法不同而已。

信号量
      信号量内核对象用于对资源进行计数。它们与所有内核对象一样,包含一个使用数量,但是它们也包含另外两个带符号的32位值,一个是最大资源数量,一个是当前资源数量。最大资源数量用于标识信标能够控制的资源的最大数量,而当前资源数量则用于标识当前可以使用的资源的数量。
   信号量的使用规则如下:
   • 如果当前资源的数量大于0,则发出信标信号。
   • 如果当前资源数量是0,则不发出信标信号。
   • 系统决不允许当前资源的数量为负值。
   • 当前资源数量决不能大于最大资源数量。
创建一个信号量内核对象
HANDLE CreateSemaphore(
    LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // pointer to security attributes
    LONG lInitialCount, // initial count
    LONG lMaximumCount, // maximum count
    LPCTSTR lpName  // pointer to semaphore-object name 
   );
       和大多数创建内核对象的函数一样,它的第一个参数用来接受安全信息,通常我们用NULL来表示默认,最后一个参数为创建这个信号量的名字,此名字可以使得我们在其他的进程中使用此信号量,lInitialCount参数代表创建信号两时允许资源访问的个数,lMaximumCount用来指定最大资源数,不要让lInitialCount大于lMaximumCount。
      使用Create***创建内核对象时,要注意一个问题,例如,如果已经有一个进程A创建了一个名为'wudi_1982'的信号量内核对象,当另外一个进程B也试图创建名字为'wudi_1982'的内核对象的时候,系统首先要查看是否已经存在一个名字为'wudi_1982'的内核对象。由于确实存在一个带有该名字的对象,因此内核要检查对象的类型。如果类型相同(例如都是信号量内核对象),此时系统会执行一次安全检查,以确定调用者是否拥有对该对象的完整的访问权。如果拥有这种访问权,系统就在进程B的句柄表中找出一个空项目,并对该项目进行初始化,使该项目指向现有的内核对象。如果该对象类型不匹配,或者调用者被拒绝访问,那么Create****将运行失败(返回NULL)。

打开一个现有的信号量
HANDLE OpenSemaphore(
    DWORD dwDesiredAccess, // access flag
    BOOL bInheritHandle, // inherit flag
    LPCTSTR lpName  // pointer to semaphore-object name 
   );
      参数dwDesiredAccess代表了访问权限,bInheritHandle参数表明子进程是否可继承,最后一个参数lpName 用于指明内核对象的名字。不能为该参数传递NULL,必须传递以0结尾的地址。这些函数要搜索内核对象的单个名空间,以便找出匹配的空间。如果不存在带有指定名字的内核对象,该函数返回NULL,GetLastError返回2(ERROR_FILE_NOT_FOUND)。但是,如果存在带有指定名字的内核对象,并且它是相同类型的对象,那么系统就要查看是否允许执行所需的访问(通过dwDesiredAccess参数进行访问)。如果拥有该访问权,调用进程的句柄表就被更新,对象的使用计数被递增。如果为bInheritHandle,参数传递TRUE,那么返回的句柄将是可继承的。调用Create*函数与调用Open*函数之间的主要差别是,如果对象并不存在,那么Create*函数将创建该对象,而Open*函数则运行失败。

通过调用ReleaseSemaphore函数,线程就能够对信标的当前资源数量进行递增
BOOL ReleaseSemaphore(
    HANDLE hSemaphore, // handle of the semaphore object 
    LONG lReleaseCount, // amount to add to current count 
    LPLONG lpPreviousCount  // address of previous count
   );
      参数hSemaphore代表了要操作内核对象的句柄,lReleaseCount表明该函数此值添加给信标的当前资源数量,通常我们用1。lpPreviousCount返回当前资源数量的原始值,大多数的时候我们并不关心这个数值,所以一般赋值为NULL。

一个例子:

{
   作者:wudi_1982
   联系方式:wudi_1982@hotmail.com
   此代码用来演示使用信号量完成线程的同步
   转载请著名出处
}


//主要代码
const
  SEMANAME
='MySema';//信号量的名字
    
//线程类声明
  TSemaThread=class(TThread)
    
private
      CurCount : integer;
//当前计数
      Flabel : TLabel;//一个用来在界面上显示当前计数的label
      procedure GetRestult;
    
protected
      procedure Execute;
override;
    
public
       constructor Create(Alabel : TLabel);
  end;

//线程类的实现代码
constructor TSemaThread.Create(Alabel: TLabel);
begin
   Flabel :
= Alabel;
   inherited Create(False);
end;

procedure TSemaThread.Execute;
//注意下面这个常量的定义
const
  SEMAPHORE_ALL_ACCESS
=$1F0003;
var
 i : integer;
  SmHandle : THandle;
begin
  inherited;
  CurCount :
= 0;
  SmHandle :
= OpenSemaphore(SEMAPHORE_ALL_ACCESS,false,SEMANAME);
  WaitForSingleObject(SmHandle,INFINITE);
  
for i := 0 to 10000 do
  begin
         Inc(CurCount);
         GetRestult;
  end;
  ReleaseSemaphore(SmHandle,
1,nil);
  CloseHandle(SmHandle);
end;

//调用此测试类的代码
procedure TSemaThread.GetRestult;
begin
   Flabel.Caption :
= IntToStr(CurCount);
end;

procedure TForm1.createTsClick(Sender: TObject);
begin
  TSemaThread.Create(labSem);
  TSemaThread.Create(labSem2);
  TSemaThread.Create(labSem3);
end;

procedure TForm1.CreateSemClick(Sender: TObject);
begin
  SmeHandle :
=
  CreateSemaphore(nil,
1,3,SEMANAME);
end;

procedure TForm1.Button13Click(Sender: TObject);
begin
  CloseHandle(SmeHandle)
end;

    对上述例子操作的说明:

   首先程序通过一个按钮来生成一个信号量内核对象,当前使用计数是1,最大为3,这里,如果你有兴趣,你可以将当前使用计数改为2,从而你可以观察到信号量内核对象和其他内核对象(例如互斥)的最大区别。当信号量已经生成之后,点击按钮创建三个线程,线程根据信号量的名字通过OpenSemaphore来打开,这样做的一个好处是,你可以同时多次执行此程序,例如你将这个程序同时打开了3个,在其中一个中,首先设置信号量,然后让其他的程序都执行线程操作,你会发现,他们依然同步的很好。这是临界区无法做到的。

  对代码的一些说明:
  1、多个进程之间来完成同步。在前面的例子中,我都是使用一个全军变量***:Thandle来记录内核对象,以使得我们可以在多个线程中访问同一个内核对象,这里,我没有再使用这个办法,而是利用了名字,只所以要这么做,是因为,如果你给内核对象起一个名字,那么你可以方便的在其他线程中使用同一个内核对象。这也是使用内核对象完成同步和使用临界区方式最大的不同,使用临界区,你只能在同一个进程中来完成同步。你完成可以将上述代码整理之后做成一个程序,然后同时执行多个此程序,来观察效果。即使不再同一个进程之中,线程依然可以很好的完成同步。
  2、Access Mask Format.在利用名字使用内核对象时,我们用到OpenSemaphore来完成操作,前面说了,它的第一个参数用来决定访问权限,事实上,其他的内核对象,例如互斥,他们的open*操作都是如此。这个用来决定权限的参数至关重要,在上面的代码中,我定义了一个常量const  SEMAPHORE_ALL_ACCESS=$1F0003;,如果你在DLEPHI中使用过互斥对象来完成同步,你会觉得不可理解,因为在使OpenMutex函数打开互斥对象时,第一个参数你可以直接使用MUTEX_ALL_ACCESS,那时因为DELPHI的windows单元中存在对它的定义,看MSDN的帮助文档,你会发现使用信号量时候,也有一个类似的已经定义的常量SEMAPHORE_ALL_ACCESS ,不过很可惜,DELPHI中并没有定义这个常量,所以我们不得不自己定义。另外只得注意的一点是,通常情况下,我们都是使用$1F0003,但有时候你不得不使用其他的权限信息,此时,你必须注意的一点是,你要让权限中包含SEMAPHORE_MODIFY_STATE(0x0002)这个信息,在MSDN中,它的说明如下,Modify state access, which is required for the ReleaseSemaphore function.你可以做将我上面的代码进行简单修来来测试,例如你将SEMAPHORE_ALL_ACCESS定义为STANDARD_RIGHTS_REQUIRED or SYNCHRONIZE,那么在程序执行的时候,你会发现只有第一个获得CPU调度的线程可以正常完成操作,而其他等待此信号量的线程将永远的等待下去,原因很简单,就是刚才贴出的MSDN上的那一句话。根据那一句话,如果你将SEMAPHORE_ALL_ACCESS 定义为$1F0002,你会发现,程序也没有问题。为什么,这就要说到OpenSemaphore的dwDesiredAccess参数,那么你就要了解Access Mask Format,可以根据下面的图来加深理解。具体参考MSDN的帮助


3、内核对象的使用计数。在线程的执行代码中,你可以看到在线程工作完成之后的CloseHandle(SmHandle)这一句,请记住,及时释放不必要的资源,是一个很好的习惯。此时,你可能会问,我的第一个线程调用了CloseHandle(SmHandle),那么我后边还没有执行的线程同样需要这个资源,是否就不能执行了呢?答案是否定的。内核对象包含了一个使用计数信息,当你Create*的时候,使用计数是1,随后,当open*的时候,使用计数加1。当你调用了一个CloseHandle时,在CloseHandle返回之前,它会清除进程的句柄表中的项目,该句柄现在对你的进程已经无效,不应该试图使用它。无论内核对象是否已经撤消,都会发生清除操作。当调用CloseHandle函数之后,将不再拥有对内核对象的访问权,不过,如果该对象的使用计数没有递减为0,那么该对象尚未被撤消。这没有问题,它只是意味着一个或多个其他进程正在使用该对象。当其他进程停止使用该对象时(通过调用CloseHandle),该对象将被撤消。

一些可以用于同步的其他内核对象
   互斥对象
   CreateMutex、ReleaseMutex、openMutex
   等待计时器对象
   CreateWaitableTimer、SetWaitableTimer

    转载请著名出处,谢谢!

抱歉!评论已关闭.