1、ManualResetEventSlim和SemaphoreSlim
在上一篇的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/线程),则使用内核模式。