C#的多线程机制探索(5)

8 篇文章 0 订阅

五、互斥对象——更加灵活的同步方式

有时候你会觉得上面介绍的方法好像不够用,对,我们解决了代码和资源的同步问题,解决了多线程自动化管理和定时触发的问题,但是如何控制多个线程相互之间的联系呢?例如我要到餐厅吃饭,在吃饭之前我先得等待厨师把饭菜做好,之后我开始吃饭,吃完我还得付款,付款方式可以是现金,也可以是信用卡,付款之后我才能离开。分析一下这个过程,我吃饭可以看作是主线程,厨师做饭又是一个线程,服务员用信用卡收款和收现金可以看作另外两个线程,大家可以很清楚地看到其中的关系——我吃饭必须等待厨师做饭,然后等待两个收款线程之中任意一个的完成,然后我吃饭这个线程可以执行离开这个步骤,于是我吃饭才算结束了。事实上,现实中有着比这更复杂的联系,我们怎样才能很好地控制它们而不产生冲突和重复呢?

这种情况下,我们需要用到互斥对象,即System.Threading命名空间中的Mutex类。大家一定坐过出租车吧,事实上我们可以把Mutex看作一个出租车,那么乘客就是线程了,乘客首先得等车,然后上车,最后下车,当一个乘客在车上时,其他乘客就只有等他下车以后才可以上车。而线程与Mutex对象的关系也正是如此,线程使用Mutex.WaitOne()方法等待Mutex对象被释放,如果它等待的Mutex对象被释放了,它就自动拥有这个对象,直到它调用Mutex.ReleaseMutex()方法释放这个对象,而在此期间,其他想要获取这个Mutex对象的线程都只有等待。

下面这个例子使用了Mutex对象来同步四个线程,主线程等待四个线程的结束,而这四个线程的运行又是与两个Mutex对象相关联的。其中还用到AutoResetEvent类的对象,如同上面提到的ManualResetEvent对象一样,大家可以把它简单地理解为一个信号灯,使用AutoResetEvent.Set()方法可以设置它为有信号状态,而使用AutoResetEvent.Reset()方法把它设置为无信号状态。这里用它的有信号状态来表示一个线程的结束。


// Mutex.cs

using System;
using System.Threading;

public class MutexSample
{
  static Mutex gM1;
  static Mutex gM2;
  const int ITERS = 100;
  static AutoResetEvent Event1 = new AutoResetEvent(false);
  static AutoResetEvent Event2 = new AutoResetEvent(false);
  static AutoResetEvent Event3 = new AutoResetEvent(false);
  static AutoResetEvent Event4 = new AutoResetEvent(false);

  public static void Main(String[] args)
  {
  Console.WriteLine("Mutex Sample ...");
  //创建一个Mutex对象,并且命名为MyMutex
  gM1 = new Mutex(true,"MyMutex");
  //创建一个未命名的Mutex 对象.
  gM2 = new Mutex(true);
  Console.WriteLine(" - Main Owns gM1 and gM2");

  AutoResetEvent[] evs = new AutoResetEvent[4];
  evs[0] = Event1; file://为后面的线程t1,t2,t3,t4定义AutoResetEvent对象
  evs[1] = Event2;
  evs[2] = Event3;
  evs[3] = Event4;

  MutexSample tm = new MutexSample( );
  Thread t1 = new Thread(new ThreadStart(tm.t1Start));
  Thread t2 = new Thread(new ThreadStart(tm.t2Start));
  Thread t3 = new Thread(new ThreadStart(tm.t3Start));
  Thread t4 = new Thread(new ThreadStart(tm.t4Start));
  t1.Start( );// 使用Mutex.WaitAll()方法等待一个Mutex数组中的对象全部被释放
  t2.Start( );// 使用Mutex.WaitOne()方法等待gM1的释放
  t3.Start( );// 使用Mutex.WaitAny()方法等待一个Mutex数组中任意一个对象被释放
  t4.Start( );// 使用Mutex.WaitOne()方法等待gM2的释放


  Thread.Sleep(2000);
  Console.WriteLine(" - Main releases gM1");
  gM1.ReleaseMutex( ); file://线程t2,t3结束条件满足

  Thread.Sleep(1000);
  Console.WriteLine(" - Main releases gM2");
  gM2.ReleaseMutex( ); file://线程t1,t4结束条件满足

  //等待所有四个线程结束
  WaitHandle.WaitAll(evs);
  Console.WriteLine("... Mutex Sample");
  Console.ReadLine();
  }

  public void t1Start( )
  {
  Console.WriteLine("t1Start started, Mutex.WaitAll(Mutex[])");
  Mutex[] gMs = new Mutex[2];
  gMs[0] = gM1;//创建一个Mutex数组作为Mutex.WaitAll()方法的参数
  gMs[1] = gM2;
  Mutex.WaitAll(gMs);//等待gM1gM2都被释放
  Thread.Sleep(2000);
  Console.WriteLine("t1Start finished, Mutex.WaitAll(Mutex[]) satisfied");
  Event1.Set( ); file://线程结束,将Event1设置为有信号状态
  }

  public void t2Start( )
  {
  Console.WriteLine("t2Start started, gM1.WaitOne( )");
  gM1.WaitOne( );//等待gM1的释放
  Console.WriteLine("t2Start finished, gM1.WaitOne( ) satisfied");
  Event2.Set( );//线程结束,将Event2设置为有信号状态
  }

  public void t3Start( )
  {
  Console.WriteLine("t3Start started, Mutex.WaitAny(Mutex[])");
  Mutex[] gMs = new Mutex[2];
  gMs[0] = gM1;//创建一个Mutex数组作为Mutex.WaitAny()方法的参数
  gMs[1] = gM2;
  Mutex.WaitAny(gMs);//等待数组中任意一个Mutex对象被释放
  Console.WriteLine("t3Start finished, Mutex.WaitAny(Mutex[])");
  Event3.Set( );//线程结束,将Event3设置为有信号状态
  }

  public void t4Start( )
  {
  Console.WriteLine("t4Start started, gM2.WaitOne( )");
  gM2.WaitOne( );//等待gM2被释放
  Console.WriteLine("t4Start finished, gM2.WaitOne( )");
  Event4.Set( );//线程结束,将Event4设置为有信号状态
  }
}


下面是该程序的执行结果:


从执行结果可以很清楚地看到,线程t2,t3的运行是以gM1的释放为条件的,而t4gM2释放后开始执行,t1则在gM1gM2都被释放了之后才执行。Main()函数最后,使用WaitHandle等待所有的AutoResetEvent对象的信号,这些对象的信号代表相应线程的结束。

  六、小结

多线程程序设计是一个庞大的主题,而本文试图在.net Framework环境下,使用最新的C#语言来描述多线程程序的概貌。希望本文能有助于大家理解线程这种概念,理解多线程的用途,理解它的C#实现方法,理解线程将为我们带来的好处和麻烦。C#是一种新的语言,因此它的线程机制也有许多独特的地方,希望大家能通过本文清楚地看到这些,从而可以对线程进行更深入的理解和探索。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您查询的关键词是:delphi 同步 数据 。如果打开速度慢,可以尝试快速版;如果想保存快照,可以添加到搜藏。 (百度和网页http://blog.csdn.net/mygodsos/archive/2008/10/19/3097921.aspx的作者无关,不对其内容负责。百度快照谨为网络故障时之索引,不代表被搜索网站的即时页面。) -------------------------------------------------------------------------------- 发呆茶馆 登录 注册 欢迎 退出 我的博客 配置 写文章 文章管理 博客首页 全站 当前博客 空间 博客 好友 相册 留言 用户操作 [发私信] [加为好友] mygodsos 订阅我的博客 mygodsos的公告 文章分类 Delphi Delphi学习--多线程 Delphi学习--自创的常用函数 期货大事记 生活感悟 投资理财 编程学习 万一的Delphi博客 存档 2009年05月(3) 2008年11月(13) 2008年10月(8) 2008年09月(3) ◆Delphi多线程编程之三 同步读写全局数据 ◆(乌龙哈里2008-10-12) 收藏 ◆Delphi多线程编程之三同步读写全局数据 ◆(乌龙哈里2008-10-12) (调试环境:Delphi2007+WinXPsp3 例程:Tst_Thread3.dpr) 开始研究最重要的多线程读写全局数据了,结合书上的例子,我修改成下面的情况: unit Tst_Thread3U; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs, StdCtrls; type TForm1 = class(TForm) Button1: TButton; Memo1: TMemo; Button2: TButton; Button3: TButton; procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); procedure Button3Click(Sender: TObject); private procedure ThreadsDone(Sender: TObject); end; TMyThread=class(TThread) protected procedure Execute;override; end; var Form1: TForm1; implementation {$R *.dfm} const MaxSize=128; var NextNumber:Integer=0; DoneFlags:Integer=0; GlobalArry:array[1..MaxSize] of Integer; Lock:byte; //1-不同步 2-临界区 3-互斥 CS:TRTLCriticalSection; //临界区 hMutex:THandle; //互斥 function GetNextNumber:Integer; begin Result:=NextNumber; inc(NextNumber); end; procedure TMyThread.Execute; var i:Integer; begin FreeOnTerminate:=True; //终止后自动free OnTerminate:=Form1.ThreadsDone; if Lock3 then //非互斥情况 begin if Lock=2 then EnterCriticalSection(CS); //建立临界区 for i := 1 to MaxSize do begin GlobalArry[i]:=GetNextNumber; Sleep(5); end; if Lock=2 then LeaveCriticalSection(CS);//离开临界区 end else //-------互斥 begin if WaitForSingleObject(hMutex,INFINITE)=WAIT_OBJECT_0 then begin for i := 1 to MaxSize do begin GlobalArry[i]:=GetNextNumber; Sleep(5); end; end; ReleaseMutex(hMutex); //释放 end; end; procedure TForm1.ThreadsDone(Sender: TObject); var i:Integer; begin Inc(DoneFlags); if DoneFlags=2 then begin for i := 1 to MaxSize do Memo1.Lines.Add(inttostr(GlobalArry[i])); if Lock=2 then DeleteCriticalSection(CS); //删除临界区 If Lock=3 then CloseHandle(hMutex); //关闭互斥 end; end; //非同步 procedure TForm1.Button1Click(Sender: TObject); begin Lock:=1; TMyThread.Create(False); TMyThread.Create(False); end; //临界区 procedure TForm1.Button2Click(Sender: TObject); begin Lock:=2; InitializeCriticalSection(CS); //初始化临界区 TMyThread.Create(False); TMyThread.Create(False); end; //互斥 procedure TForm1.Button3Click(Sender: TObject); begin Lock:=3; // 互斥 hMutex:=CreateMutex(0,False,nil); TMyThread.Create(False); TMyThread.Create(False); end; end. 没有临界区和互斥的帮助,两个线程都不断地在Memo1输出,而且数字是乱的。 一、临界区 所谓临界区,就是一次只能由一个线程来执行的一段代码。如果把初始化数组的代码放在临界区内,另一个线程在第一个线程处理完之前是不会被执行的。 使用临界区的步骤: 1、先声明一个全局变量类型为TRTLCriticalSection; 2、在线程Create()前调用InitializeCriticalSection()过程来初始化,该函数定义是: void WINAPI InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection); 类型lpCriticalSection即是Delphi封装的TRTLCriticalSection。 3、在线程的需要放入临界区的代码前面使用EnterCriticalSection(lpCriticalSection)过程来开始建立临界区。在代码完成后用LeaveCriticalSection(lpCriticalSection)来标志临界区的结束。 4、在线程执行完后用DeleteCriticalSection(lpCriticalSection)来清除临界区。这个清除过程必须放在线程执行完后的地方,比如FormDesroy事件中。上面的例子中,若把该过程放在TMyThread.Create(False);后,会产生错误。 二、互斥: 互斥非常类似于临界区,除了两个关键的区别:首先,互斥可用于跨进程的线程同步。其次,互斥能被赋予一个字符串名字,并且通过引用此名字创建现有互斥对象的附加句柄。 提示临界区与事件对象(比如互斥对象)的最大的区别是在性能上。临界区在没有线程冲突时,要用10~15个时间片,而事件对象由于涉及到系统内核要用400~600个时间片。 使用互斥的步骤: 1、声明一个类型为Thandle或Hwnd的全局变量,其实都是Cardinal类型。Hwnd是handle of window,主要用于窗口句柄;而Thandle则没有限制。 2、线程Create()前用CreateMutex()来创建一个互斥量。该函数定义为: HANDLE WINAPI CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName:Pchar); LPSECURITY_ATTRIBUTES参数为一个指向TSecurityAttributtes记录的指针。此参数设为nil,表示访问控制列表默认的安全属性。 bInitalOwner参数表示创建互斥对象的线程是否要成为此互斥对象的拥有者。当此参数为False时,表示互斥对象没有拥有者。 lpName参数指定互斥对象的名称。设为nil表示无命名,如果参数不是设为nil,函数会搜索是否有同名的互斥对象存在。如果有,函数就会返回同名互斥对象的句柄。否则,就新创建一个互斥对象并返回其句柄。 返回值是一handle。当错误发生时,返回null,此时用GetLastError函数可查看错误的信息。 利用CreateMutex()可以防止程序多个实例运行,如下例: Program ABC; Uses Forms,Windows,…; {$R *.res} Var hMutex:Hwnd; Begin Application.Initialize; hMutex:=CreateMutex(nil,False,Pchar(Application.Title)); if GetLastErrorERROR_ALREADY_EXISTS then begin //项目要运行的咚咚 end; ReleaseMutex(hMutex); Application.Run; End; 在本节的例程中,我们只是要防止线程进入同步代码区域中,所以lpName参数设置为nil。 3、在同步代码前用WaitForSingleObject()函数。该函数使得线程取得互斥对象(同步代码)的拥有权。该函数定义为: DWORD WINAPI WaitForSingleObject( HANDLE hHandle, DWORD dwMilliseconds); 这个函数可以使当前线程在dwMilliseconds指定的时间内睡眠,直到hHandle参数指定的对象进入发信号状态为止。一个互斥对象不再被线程拥有时,它就进入发信号状态。当一个进程要终止时,它就进入发信号状态。dwMilliseconds参数可以设为0,这意味着只检查hHandle参数指定的对象是否处于发信号状态,而后立即返回。dwMilliseconds参数设为INFINITE,表示如果信号不出现将一直等下去。 这个函数的返回值含义: WAIT_ABANDONED 指定的对象是互斥对象,并且拥有这个互斥对象的线程在没有释放此对象之前就已终止。此时就称互斥对象被抛弃。这种情况下,这个互斥对象归当前线程所有,并把它设为非发信号状态 WAIT_OBJECT_0 指定的对象处于发信号状态 WAIT_TIMEOUT 等待的时间已过,对象仍然是非发信号状态 再次声明,当一个互斥对象不再被一个线程所拥有,它就处于发信号状态。此时首先调用WaitForSingleObject()函数的线程就成为该互斥对象的拥有者,此互斥对象设为不发信号状态。当线程调用ReleaseMutex()函数并传递一个互斥对象的句柄作为参数时,这种拥有关系就被解除,互斥对象重新进入发信号状态。 注意除WaitForSingleObject()函数外,你还可以使用WaitForMultipleObject()和MsgWaitForMultipleObject()函数,它们可以等待几个对象变为发信号状态。这两个函数的详细情况请看Win32 API联机文档。 4、在同步代码结束后,使用ReleaseMutex(THandle)函数来标志。该函数只是释放互斥对象和线程的拥有者关系,并不释放互斥对象的句柄。 5、调用CloseHandle(THandle)来关闭互斥对象。请注意例程中该函数的使用位置。 三、还有一种用信号量对象来管理线程同步的,它是在互斥的基础上建立的,但信号量增加了资源计数的功能,预定数目的线程允许同时进入要同步的代码。有点复杂,想不到在哪可以用,现在就不研究论了。 发表于 @ 2008年10月19日 00:47:00 | 评论( loading... ) | 编辑| 举报| 收藏 旧一篇:◆delphi多线程编程之二 ◆(乌龙哈里2008-10-12) | 新一篇:◆Delphi多线程编程之四 线程安全和VCL ◆(乌龙哈里2008-10-12)Csdn Blog version 3.1a Copyright © mygodsos

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值