《CLR via C#》读书笔记-线程同步(五)


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/线程),则使用内核模式。






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值