首页 > 学院 > 开发设计 > 正文

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

2019-11-06 06:58:35
字体:
来源:转载
供稿:网友

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


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表