第14章 线程和异步编程
14.3线程安全
教学提示 : 本节主要达到目的。
? 了解线程安全的相关知识点。(略讲)
? 了解同步上下文和同步代码区域相关知识点及手动同步的技术。(精讲) ? 了解线程安全和.Net Framework类的相关知识点。(略讲) 教学内容 讲授: 教学方法 阅书:14.3.1 提示 首先介绍线程安全概述。什么叫线程安全?幻灯:第15-16页 操作系统在运行程序时,程序内部的多个线程同时在运行,因为线程的运行状态完全是 由操作系统调度的,因此如果没有对线程的 运行做一定的控制,当两个或者多个线程访问同一个资源或者共享数据的时候就会产生资源争用,有的线程要读取数据,有的线程要写入数据等。当多个线程都在操作这些数据时就会导致错误;另外一个问题是死锁,每个线程在等待对方完成一定的任务,只有对方完成了这个任务以后才能执行下一个任务,在最极端的情况下,所有的线程都在互相等待,这时程序就没法继续运行了。在多线程编程时,难免会遇到这些问题,必须想办法解决。最根本的办法是,不共享数据,也就是说每一个线程使用的数据都是与本线程相关的数据,而不和其他线程共用一块数据。但是在实际编程当中,这种情况很难做到,如果一个线程仅仅访问自己的数据而不和别的线程进行协作,那么多线程编程的功能和效用就会大打折扣,在多线程编程时必然要和其他线程进行通信并访问共同的数据。.NET Framework提供了几种不同的办法,让线程在执行方法访问数据时,保证其安全性,也就是要避免争用的情况发生、避免同时修改一个数据、避免操作数据时导致逻辑的混乱、避免死锁。可以通过以下几种方式避免争用和死锁:同步上下文、同步代码区域和手动同步
11
《基于VB.NET的.NET Framework程序设计》教学指导手册—详细教案
讲解课本:14.3.1 讲授: 阅书:14.3.2 幻灯:第17页 代码里有这样一个属性:Synchronization——同步,这个属性可以为 ContextBoundObject类的对象(CounterSynchronizedContext)提供自动的 线程同步功能。如果要使用同步上下文功能,那么必须把这个线程函数包含在类中,而且这个类必须从ContextBoundObject类继承,同时在这个类前面加上Synchronization属性。经过这两步操作以后,就能同步并保护线程里的成员字段和成员方法,也就是说即使有多个线程要访问这个类里面的成员字段,比如iCount,一次也只允许一个线程访问。例如,ABCD四个线程都要访问这个数据,但同时只有一个能对它进行读写操作,其他线程都必须等待,只有等到前面一个线程完成以后,其他线程才可以一个一个轮流去操作这个数据。对于成员方法也是一样的道理, Increment方法也是一个成员方法,一次也只允许一个线程调用这个方法。使用专门的类进行继承,再用上Synchronization属性,就可以保证线程里的成员方法和成员字段都是线程安全的,不可能有多个线程同时去访问到它。但是需要注意的是,这里所说的仅仅是成员字段和成员方法,类里包含的静态方法或静态成员变量。从上述分析可以看出,使用这种方式来确保线程安全是非常简单的,不需要写任何额外的代码,只需要类从一个特定的类继承,再在类前面加一个属性,即能保证类里的所有成员字段和成员方法都是同步的。当然,它也有一些限制,其一,类必须从ContextBoundObject类继承,.Net只有单继承机制,有时候不一定会满足这种情况;其二,这种方法不能保护静态变量,对静态变量还要做特殊处理。对于受保护的成员方法和成员字段来说,也有一定的问题,这种同步的方法仅在较高的层次上保证成员方法和成员字段每次只被一个线程访问,实际上这种方法的效率不是很高,若要提高同步的效率,就必须在更细的级别上,比如对某几行代码或某一块代码区域实施保护,而不是整个方法的保护,因此12
第14章 线程和异步编程
这种方法简便,但是灵活性不是很好。 讲解课本:14.3.2 讲授: 阅书:14.3.3 Monitor.Wait方法参照: http://msdn.microsoft.com/library/chs/default.asp?url=/library/CHS/cpref/html/frlrfsystemthreadingmonitorclasswaittopic.asp Monitor.Pulse方法参照: http://msdn.microsoft.com/library/chs/default.asp?url=/library/CHS/cpref/html/frlrfsystemthreadingmonitorclasswaittopic.asp 下面介绍另一种方法——同步代码区域,这幻灯:第18页 种方式比上面提到的同步上下文更具灵活 性。它使用MethodImplAttribute属性来保证同步,从这个属性的名字可以看出,它是 用来修饰方法的。和前面介绍的Synchronization不一样, Synchronization用来修饰一个类,保证这个类里所有成员方法的同步,但可以把MethodImplAttribute属性放在某一个方法前面,通过这种方式确保所修饰的方法只能够在一个线程中运行;除了使用这个属性,我们还可以用类来实现同步,可以使用Monitor 类,Monitor类的用途是什么呢?可以通过调用Monitor类的方法来告诉系统,从哪一条语句开始进入同步区域,然后再调用Monitor类的方法退出同步区域。Monitor 类有几个最基本的方法:调用Monitor类的Monitor.Enter方法后,会通知系统现在进入同步区域,与前面的Synchronization属性、和MethodImpl属性相比,Monitor方法的好处是,它可以保护某一个方法的一部分,也就是说它既可以保护整个方法,也可以通过在方法的开头使用Monitor.Enter,在方法的结尾Monitor.Exit来保护整个方法,还可以保护方法的一部分代码片断,而不是整个方法。另外在VB和C#里,分别有两个关键字:lock和SyncLock。这两个关键字的用途类似,它的内部实现其实是Monitor,因此与Monitor类的原理相同,只不过在语言级别上通过关键字来支持代码的同步区域,所以可以用lock(this)来保护lock方法所在的类,即保证类中实例方法的同步,也可用SyncLock,然后用SyncLock方法所在类的类型SyncLock GetType (class) 获得这个类的类型,此时保护的就是和这个类的类型相关的静态成员。lock也可这样使用。
13
《基于VB.NET的.NET Framework程序设计》教学指导手册—详细教案
SyncLock和lock分别是VB.Net和C#里的两个关键字,它们既可以用来保护静态方法和静态成员,也可以用来保护实例成员和实例方法。若要保护静态成员,就应该用SyncLock GetType (class) 或者lock (typeof( )),例如typeof(Counter),其中Counter是所在类的类型;若要保护实例类型,应该写成lock(this)或者SyncLock(me),这样就可以分别保护C#里自身类的实例成员或者VB.Net里自身的实例成员。这几个方法在本质上都使用Monitor类来实现的,但是用语言的关键字显得比较方便一些。 讲解课本14.3.3 讲授: 阅书:14.3.4 最后介绍第三种方法——手动同步。使幻灯:第19页 用Interlocked类来进行手动同步,这个类可同步多线程共享的数据。在实现此类时, 利用了现代CPU内部的一些高级功能和指令,因此,可以在同步时获得最高性能,例如,在保证线程安全的情况下,交换两个数据或者对一个数字进行加1或减1操作,使用Interlocked类的Decrement、 Exchange、Increment方法,可以保证最高的效率,当然也可以把操作自定义成一个方法,用前面介绍的属性、Lock和Monitor关键字或者类来同步这个方法,但是这样做的效率没有Interlocked高,因此在处理数据时,如果只需要对多线程之间共享的变量做操作,比如线程A想增加一个变量的值,但是该变量同时被线程B、C使用,那么在增加变量的值时,就不能够直接获取它的值(例如使用i=i+1),而应该使用Interlocked . Increment来增加变量的值,以保证在用Increment增加值时,这个变量只被正在运行的这个线程所操作,不会被其他线程干涉,保证数据的使用操作不会出现错误。因此,Interlocked 类的这种手动方式主要用来处理共享内存中不同线程都可以访问的共享变量。 此外,还有其他几种手动同步方法。可以使用Mutex 类在多个线程之间进行同步,可14
第14章 线程和异步编程
以像使用Monitor类一样,使用Mutex类来同步代码区域段,但是它和Monitor类又有一些区别:Mutex类实际上是操作系统里固有的、用于线程之间同步和用于线程和进程之间同步的资源。Mutex类可以跨进程进行同步,也就是说,在运行两个完全不同的程序时,这两个程序可以通过Mutex 类进行协作,以保证代码之间的同步,但是Monitor类做不到这一点,它只能在一个.Net程序里实现同步。 熟悉了Mutex类后,再来了解另一个手动进入同步区域和退出同步区域的类ReadWriteLock,它主要的功能是支持单写多读,也就是说,具有满足如下条件的数据:在我这个数据进行操作时,允许同时有多个线程读取这个数据,但只允许一个线程在同一个时刻对它进行修改,而且为了优化性能,还规定多线程在同时读或写数据时,写线程的优先级比读线程高。例如,现在有三个线程在排队,等待读取数据,第四个线程准备修改数据,那么该线程就会排在第三个线程之后,以后进来的所有读线程都会紧跟在写线程后面,只有当最先的三个线程读取完毕后,第四个进入这个区域的线程,即写线程,才能修改数据,而后面的读线程都会被写线程阻塞,等修改完毕后才能读取数据。 要操作这样一个类,必须声明两个方法:Read()和Write(),这两个方法之间通过ReadWriteLock 类进行同步。要读取数据时,调用AcquireReaderLock方法,即获得ReaderLock,获得ReaderLock后就能读取数据了,此后的所有写操作都被阻塞;反之亦然,读完数据后,要调用ReleaseWriterLock()方法。下面观察Write方法,Write方法里的 AcquireWriterLock会阻塞其他写线程,但如果有线程在读取数据,那么调用AcquireWriterLock方法也会阻塞。只有当所有读线程都调用了ReleaseReaderLock后,才会释放所有的读
15