时间:2016-10-26来源:本站原创作者:佚名

本文主要描述在C#中线程同步的方法。线程的基本概念网上资料也很多就不再赘述了。直接接入主题,在多线程开发的应用中,线程同步是不可避免的。在.Net框架中,实现线程同步主要通过以下的几种方式来实现,在MSDN的线程指南中已经讲了几种,本文结合作者实际中用到的方式一起说明一下。

1.维护自由锁(InterLocked)实现同步

2.监视器(Monitor)和互斥锁(lock)

3.读写锁(ReadWriteLock)

4.系统内核对象

1)互斥(Mutex),信号量(Semaphore),事件(AutoResetEvent/ManualResetEvent)

2)线程池

除了以上的这些对象之外实现线程同步的还可以使用Thread.Join方法。这种方法比较简单,当你在第一个线程运行时想等待第二个线程执行结果,那么你可以让第二个线程Join进来就可以了。

自由锁(InterLocked)

对一个32位的整型数进行递增和递减操作来实现锁,有人会问为什么不用++或--来操作。因为在多线程中对锁进行操作必须是原子的,而++和--不具备这个能力。InterLocked类还提供了两个另外的函数Exchange,CompareExchange用于实现交换和比较交换。Exchange操作会将新值设置到变量中并返回变量的原来值:intoVal=InterLocked.Exchange(refval,1)。

监视器(Monitor)

在MSDN中对Monitor的描述是:Monitor类通过向单个线程授予对象锁来控制对对象的访问。

Monitor类是一个静态类因此你不能通过实例化来得到类的对象。Monitor的成员可以查看MSDN,基本上Monitor的效果和lock是一样的,通过加锁操作Enter设置临界区,完成操作后使用Exit操作来释放对象锁。不过相对来说Monitor的功能更强,Moniter可以进行测试锁的状态,因此你可以控制对临界区的访问选择,等待or离开,而且Monitor还可以在释放锁之前通知指定的对象,更重要的是使用Monitor可以跨越方法来操作。Monitor提供的方法很少就只有获取锁的方法Enter,TryEnter;释放锁的方法Wait,Exit;还有消息通知方法Pulse,PulseAll。经典的Monitor操作是这样的:

//通监视器来创建临界区staticpublicvoidDelUser(stringname){try{//等待线程进入Monitor.Enter(Names);Names.Remove(name);Console.WriteLine(Del:{0},Names.Count);Monitor.Pulse(Names);}finally{//释放对象锁Monitor.Exit(Names);}}}

其中Names是一个List,这里有一个小技巧,如果你想声明整个方法为线程同步可以使用方法属性:

//通过属性设置整个方法为临界区[MethodImpl(MethodImplOptions.Synchronized)]staticpublicvoidAddUser(stringname){Names.Add(name);Console.WriteLine(Add:{0},Names.Count);}

对于Monitor的使用有一个方法是比较诡异的,那就是Wait方法。在MSDN中对Wait的描述是:释放对象上的锁以便允许其他线程锁定和访问该对象。

这里提到的是先释放锁,那么显然我们需要先得到锁,否则调用Wait会出现异常,所以我们必须在Wait前面调用Enter方法或其他获取锁的方法,如lock,这点很重要。对应Enter方法,Monitor给出来另一种实现TryEnter。这两种方法的主要区别在于是否阻塞当前线程,Enter方法在获取不到锁时,会阻塞当前线程直到得到锁。不过缺点是如果永远得不到锁那么程序就会进入死锁状态。我们可以采用Wait来解决,在调用Wait时加入超时时限就可以。

if(Monitor.TryEnter(Names)){Monitor.Wait(Names,);//!!Names.Remove(name);Console.WriteLine(Del:{0},Names.Count);Monitor.Pulse(Names);}

互斥锁(lock)

lock关键字是实现线程同步的比较简单的方式,其实就是设置一个临界区。在lock之后的{...}区块为一个临界区,当进入临界区时加互斥锁,离开临界区时释放互斥锁。MSDN对lock关键字的描述是:lock关键字可将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。

具体例子如下:

staticpublicvoidThreadFunc(objectname){stringstr=nameasstring;Randomrand=newRandom();intcount=rand.Next(,);for(inti=0;icount;i++){lock(NumList){NumList.Add(i);Console.WriteLine({0}{1},str,i);}}}

对lock的使用有几点建议:对实例锁定lock(this),对静态变量锁定lock(typeof(val))。lock的对象访问权限最好是private,否则会出现失去访问控制现象。

读写锁(ReadWriteLock)

读写锁的出现主要是在很多情况下,我们读资源的操作要多于写资源的操作。但是如果每次只对资源赋予一个线程的访问权限显然是低效的,读写锁的优势是同时可以有多个线程对同一资源进行读操作。因此在读操作比写操作多很多,并且写操作的时间很短的情况下使用读写锁是比较有效率的。读写锁是一个非静态类所以你在使用前需要先声明一个读写锁对象:

staticprivateReaderWriterLock_rwlock=newReaderWriterLock();

读写锁是通过调用AcquireReaderLock,ReleaseReaderLock,AcquireWriterLock,ReleaseWriterLock来完成读锁和写锁控制的

staticpublicvoidReaderThread(intthrdId){try{//请求读锁,如果ms超时退出_rwlock.AcquireReaderLock(10);try{intinx=_rand.Next(_list.Count);if(inx_list.Count)Console.WriteLine({0}thread{1},thrdId,_list[inx]);}finally{_rwlock.ReleaseReaderLock();}}catch(ApplicationException)//如果请求读锁失败{Console.WriteLine({0}threadgetreaderlockouttime!,thrdId);}}staticpublicvoidWriterThread(){try{//请求写锁_rwlock.AcquireWriterLock();try{stringval=_rand.Next().ToString();_list.Add(val);//写入资源Console.WriteLine(writerthreadhaswritten{0},val);}finally{//释放写锁_rwlock.ReleaseWriterLock();}}catch(ApplicationException){Console.WriteLine(Getwriterthreadlockouttime!);}}

如果你想在读的时候插入写操作请使用UpgradeToWriterLock和DowngradeFromWriterLock来进行操作,而不是释放读锁。

staticprivatevoidUpgradeAndDowngrade(intthrdId){try{_rwlock.AcquireReaderLock(10);try{try{//提升读锁到写锁LockCookielc=_rwlock.UpgradeToWriterLock();try{stringval=_rand.Next().ToString();_list.Add(val);Console.WriteLine(UpgradeThread{0}add{1},thrdId,val);}finally{//下降写锁_rwlock.DowngradeFromWriterLock(reflc);}}catch(ApplicationException){Console.WriteLine({0}threadupgradereaderlockfailed!,thrdId);}}finally{//释放原来的读锁_rwlock.ReleaseReaderLock();}}catch(ApplicationException){Console.WriteLine({0}threadgetreaderlockouttime!,thrdId);}}

这里有一点要注意的就是读锁和写锁的超时等待时间间隔的设置。通常情况下设置写锁的等待超时要比读锁的长,否则会经常发生写锁等待失败的情况。

系统内核对象互斥对象(Mutex)

互斥对象的作用有点类似于监视器对象,确保一个代码块在同一时刻只有一个线程在执行。互斥对象和监视器对象的主要区别就是,互斥对象一般用于跨进程间的线程同步,而监视器对象则用于进程内的线程同步。互斥对象有两种:一种是命名互斥;另一种是匿名互斥。在跨进程中使用到的就是命名互斥,一个已命名的互斥就是一个系统级的互斥,它可以被其他进程所使用,只要在创建互斥时指定打开互斥的名称就可以。在.Net中互斥是通过Mutex类来实现。

其实对于OpenExisting函数有两个重载版本,

Mutex.OpenExisting(String)

Mutex.OpenExisting(String,MutexRights)

对于默认的第一个函数其实是实现了第二个函数MutexRights.Synchronize

MutexRights.Modify操作。

由于监视器的设计是基于.Net框架,而Mutex类是系统内核对象封装了win32的一个内核结构来实现互斥,并且互斥操作需要请求中断来完成,因此在进行进程内线程同步的时候性能上要比互斥要好。

典型的使用Mutex同步需要完成三个步骤的操作:1.打开或者创建一个Mutex实例;2.调用WaitOne()来请求互斥对象;3.最后调用ReleaseMutex来释放互斥对象。

staticpublicvoidAddString(stringstr){//设置超时时限并在wait前退出非默认托管上下文if(_mtx.WaitOne(,true)){_resource.Add(str);_mtx.ReleaseMutex();}}

需要注意的是,WaitOne和ReleaseMutex必须成对出现,否则会导致进程死锁的发生,这时系统(.Net2.0)框架会抛出AbandonedMutexException异常。

信号量(Semaphore)

信号量就像一个夜总会:它有确切的容量,并被保镖控制。一旦满员,就没有人能再进入,其他人必须在外面排队。那么在里面离开一个人后,队头的人就可以进入。信号量的构造函数需要提供至少两个参数-现有的人数和最大的人数。

信号量的行为有点类似于Mutex或是lock,但是信号量没有拥有者。任意线程都可以调用Release来释放信号量而不像Mutex和lock那样需要线程得到资源才能释放。

classSemaphoreTest{staticSemaphores=newSemaphore(3,3);//当前值=3;容量=3staticvoidMain(){for(inti=0;i10;i++)newThread(Go).Start();}staticvoidGo(){while(true){s.WaitOne();Thread.Sleep();//一次只有个线程能被处理s.Release();}}}事件(ManualResetEvent/AutoResetEvent)src=







































哪家医院看白癜风好
早期白癜风多久能治好

转载请注明原文网址:http://www.helimiaopu.com/hjpz/1173.html

------分隔线----------------------------