在上一篇的AnotherHybridLock和SimpleHybridLock类中,均在方法内部定义了一个内核对象类(AutoResetEvent),因此在AnotherHybridLock和SimpleHybridLock实例化时,同样也会实例化一个AutoResetEvent对象,相比于int字段所产生的开销,实例化AutoResetEvent会造成较大的损失。因此.NET中定义了两个类:ManualResetEventSlim和SemaphoreSlim,这两者与SimpleHybridLock、AnotherHybridLock相比,在多线程同时访问锁时,只有在第一次检测到竞争时,才会创建AutoResetEvent,这样就避免了一些无谓的性能损失。
SimpleHybridLock类的一些属性、方法如下:
public class ManualResetEventSlim:IDisposable{ //方法 public ManualResetEventSlim(bool initialState,int spinCount); public void Dispose(); public void Reset(); public void Set(); public bool Wait(int milliSecondsTimeout, CancellationToken token); //属性 public bool IsSet{get;}; public int SpinCount{get;} public WaitHandle WaitHandle{get;}} SemaphoreSlim类的属性、方法如下:public class SemaphoreSlim:IDisposable{ //方法 public SemaphoreSlim(int initialCount,int maxCount); public void Dispose(); public int Release(int releaseCount); public bool Wait(int milliSecondsTimeout, CancellationToken token); //属性 public int CurrentCount{get;} public WaitHandle AvailableWaitHandle{get;}} 以上是两个类经常用到的相关方法和属性。2、Monitor类和同步块
Monitor类中最常用的方法有两个:
public static class Monitor{ public static void Enter(object obj); public static void Exit(object obj)} 这两个方法接收任何堆对象的引用,并对指定对象(即刚才说的引用)的同步块字段进行操作。操作同步块的具体逻辑如下:任何一个堆对象在初始化时会包含三部分:类型对象指针、同步块索引、对象的实例字段。同步块包含的内容有:一个内核对象、拥有线程的ID、一个递归计数、一个等待线程计数。CLR初始化时,CLR会为自己分配一个同步块数组。当一个对象在构造时,对象的同步块索引初始化为-1,代表对象不引用任何同步块。当使用Monitor.Enter(obj)方法时,CLR会在数组中找到一个空白的同步块,并设置对象的同步块索引来引用改同步块。即,同步块与对象的关联是动态关联的。当调用Monitor.Exit(obj)方法时,CLR会检查1、本对象的递归计数是否为0;2、是否还有其他线程在等待使用本对象。若递归计数不为0,则需要等到线程递归计数为0。若还存在其他的线程等待使用本对象,则更改拥有本对象的同步块的线程ID字段,使字段存储正在等待线程的ID。若递归计数为0,外部线程等待数为0,则CLR会将对象的同步块索引设定为-1。具体图示如下所示:
其中绿色代表对象的实例字段;红色代表同步块索引;蓝色代表类型对象指针。
3、Monitor类的问题总结
3.1 锁的公共与私有
Monitor类原本的使用方式如下:
internal sealed class Transaction{ //定义一个变量 PRivate DateTime m_timeOfLastTrans; //赋值方法,通过Monitor的Enter方法,获取同步锁 //然后完成m_timeOfLastTrans变量的赋值 public void PerformTransaction(){ //通用的使用方式就是使用this关键词,获取同步锁 Monitor.Enter(this); m_timeOfLastTrans=DateTime.Now; Monitor.Exit(this); } //属性。获取m_timeOfLastTrans的值 public DateTime LastTransaction{ get{ Monitor.Enter(this); DateTime temp = m_timeOfLastTrans; Monitor.Exit(this); return temp; } }} 上面的代码中,在使用Monitor的Enter方法时,经常使用this关键词。一般情况下,此方式是能够保证同步锁正常获取的,程序是可以正常运行的。但是若存在同步锁的嵌套时,就会存在问题。如下所示:public static void SomeMethod(){ var t=new Transaction(); //获取Transaction对象公开的锁 Monitor.Enter(t); //调用Transaction对象的LastTransaction属性 //而LastTransaction属性中调用了Enter方法 //因此,线程池内的线程会阻塞,等待Transaction对象的锁被释放 ThreadPool.QueueUserWorkItem(o=>Console.WriteLine(t.LastTransaction)); //退出操作的相关内容 Monitor.Exit(t);} 因此,为了避免出现以上“锁嵌套”的问题,建议坚持使用一个私有锁。即在使用Monitor.Enter()方法时,自定义一个单独的私有变量锁,而不是所有Enter都调用同一个变量锁。按这个思路将Transaction类进行改造下:internal sealed class Transaction{ //定义一个私有变量锁 private readonly object m_lock = new object(); private DateTime m_timeOfLastTrans; //赋值方法,通过Monitor的Enter方法,获取同步锁 //然后完成m_timeOfLastTrans变量的赋值 public void PerformTransaction(){ //通用的使用方式就是使用this关键词,获取同步锁 Monitor.Enter(m_lock); m_timeOfLastTrans=DateTime.Now; Monitor.Exit(m_lock); } //属性。获取m_timeOfLastTrans的值 public DateTime LastTransaction{ get{ Monitor.Enter(m_lock); DateTime temp = m_timeOfLastTrans; Monitor.Exit(m_lock); return temp; } }}3.2 Monitor类的问题总结
此部分问题集中出现在AppDomain中。留白,待下周补充。
3.3 Lock关键词
在C#中提供了一个关键字lock,此关键字的用途类似using,先获取一个锁,执行相关的操作,然后释放锁。示例:
private void SomeMethod(){ //类似using的用法 lock(this){ //相关的操作代码 }} lock关键词的等价写法如下:private void SomeMethod(){ bool lockToken=false; try{ //有可能在这时线程退出 Monitor.Enter(this,ref lockToken); //相关的执行代码 }finally{ if(lockToken) Monitor.Exit(this); }} 先说一下此处Enter方法的参数public static void Enter(object obj,ref bool lockToken) lockToken参数是指:若线程调用Monitor.Enter,且成功获取了锁,则将其置为true,是线程真正的获得锁的标志位。因此上面的代码就不难理解了。但是lock关键字的使用时,还是可能出现如下问题:1、在调用Monitor.Enter时,此时会更改lockToken的状态,若在更改状态时候发生异常,导致lockToken状态为true,但是线程未获得锁。此种情况,程序执行finally语句块时就会出现异常。2、try语句块需要入出栈,因此性能会下降。因此作者建议杜绝使用lock语句。但是以我的个人理解,作者说的情况会有一定概率的发生,但是这概率和对性能的影响应该是可以忽略不计的。4 线程同步小结
总的来说一句话,尽量不要使用线程同步。若不得不用,两句话:能保证耗时短的,用用户模式;不能保证的(例如跨AppDomain/线程),则使用内核模式。
新闻热点
疑难解答