解决方案:一个资源,一次只允许一个线程使用,其他线程只能等待。直到资源被释放。
问题抽象:当某一资源可能同时被多个线程读取和修改时,资源的状态将变得难以预料。
线程同步方案:volatile、lock、Interlocked、Moniter、SpinLock、ReadWriteLockSlim、Mutex
方案特性:除所有者外,其他人无条件等待;先到先得
各方案间的区别:这些方案从它们各自的实现方式可分为三种:用户模式构造、内核模式构造 和 混合模式构造。
应该尽量使用用户模式构造,它们的速度要显著快于内核模式的构造。这是因为它们使用了特殊 CPU 指令来协调线程。这意味着协调是在硬件中发生的(所以才这么快)。它们有一个缺点:只有 Windows 操作系统内核才能停止一个线程的运行(以避免浪费 CPU 时间)。所以,一个线程想要取得一个资源但又暂时取不到,它会一直在用户模式中运行。这可能浪费大量 CPU 时间。
内核模式的构造是由 Windows 操作系统自身提供的。所以,它们要求你在应用程序的线程中调用在操作系统内核中实现的函数。将线程从用户模式切换为内核模式(或相反)会招致巨大的性能损失,这正是为什么应该避免使用内核模式构造的原因。然后,它们有一个重要的优点:一个线程使用一个内核模式的构造获取一个由其它线程拥有的资源时,Windows会阻塞线程,使它不再浪费 CPU 时间。然后,当资源变得可用时,Windows 会恢复线程,允许它访问资源。
---- 《CLR via C# (第 3 版)》 P706
一、volatile关键字
volatile是最简单的一种同步方法,当然简单是要付出代价的。它只能在变量一级做同步,volatile的含义就是告诉处理器, 不要将我放入工作内存, 请直接在主存操作我。(【转自www.bitsCN.com 】)因此,当多线程同时访问该变量时,都将直接操作主存,从本质上做到了变量共享。
能够被标识为volatile的必须是以下几种类型:(摘自MSDN)
- Any reference type.
- Any pointer type (in an unsafe context).
- The types sbyte, byte, short, ushort, int, uint, char, float, bool.
- An enum type with an enum base type of byte, sbyte, short, ushort, int, or uint.
如:
public class A { private volatile int _i; public int I { get { return _i; } set { _i = value; } } }
但volatile并不能实现真正的同步,因为它的操作级别只停留在变量级别,而不是原子级别。如果是在单处理器系统中,是没有任何问题的,变量在主存中没有机会被其他人修改,因为只有一个处理器,这就叫作processor Self-Consistency。但在多处理器系统中,可能就会有问题。 每个处理器都有自己的data cach,而且被更新的数据也不一定会立即写回到主存。所以可能会造成不同步,但这种情况很难发生,因为cach的读写速度相当快,flush的频率也相当高,只有在压力测试的时候才有可能发生,而且几率非常非常小。
二、lock关键字
lock是一种比较好用的简单的线程同步方式,它是通过为给定对象获取互斥锁来实现同步的。它可以保证当一个线程在关键代码段的时候,另一个线程不会进来,它只能等待,等到那个线程对象被释放,也就是说线程出了临界区。用法:
public void Function() { object lockThis = new object (); lock (lockThis) { // Access thread-sensitive resources. } }
lock的参数必须是基于引用类型的对象,不要是基本类型像bool,int什么的,这样根本不能同步,原因是lock的参数要求是对象,如果传入int,势必要发生装箱操作,这样每次lock的都将是一个新的不同的对象。最好避免使用public类型或不受程序控制的对象实例,因为这样很可能导致死锁。特别是不要使用字符串作为lock的参数,因为字符串被CLR“暂留”,就是说整个应用程序中给定的字符串都只有一个实例,因此更容易造成死锁现象。建议使用不被“暂留”的私有或受保护成员作为参数。其实某些类已经提供了专门用于被锁的成员,比如Array类型提供SyncRoot,许多其它集合类型也都提供了SyncRoot。
所以,使用lock应该注意以下几点:
1.如果一个类的实例是public的,最好不要lock(this)。因为使用你的类的人也许不知道你用了lock,如果他new了一个实例,并且对这个实例上锁,就很容易造成死锁。
2.如果MyType是public的,不要lock(typeof(MyType))
3.永远也不要lock一个字符串
三、System.Threading.Interlocked
对于整数数据类型的简单操作,可以用 Interlocked 类的成员来实现线程同步,存在于System.Threading命名空间。Interlocked类有以下方法:Increment , Decrement , Exchange 和CompareExchange 。
使用Increment 和Decrement 可以保证对一个整数的加减为一个原子操作。
Exchange 方法自动交换指定变量的值。
CompareExchange 方法组合了两个操作:比较两个值以及根据比较的结果将第三个值存储在其中一个变量中。
比较和交换操作也是按原子操作执行的。如:
int i = 0 ; System.Threading.Interlocked.Increment( ref i); Console.WriteLine(i); System.Threading.Interlocked.Decrement( ref i); Console.WriteLine(i); System.Threading.Interlocked.Exchange( ref i, 100 ); Console.WriteLine(i); System.Threading.Interlocked.CompareExchange( ref i, 10 , 100 );
四、Monitor
Monitor类提供了与lock类似的功能,不过与lock不同的是,它能更好的控制同步块,当调用了Monitor的Enter(Object o)方法时,会获取o的独占权,直到调用Exit(Object o)方法时,才会释放对o的独占权,可以多次调用Enter(Object o)方法,只需要调用同样次数的Exit(Object o)方法即可,Monitor类同时提供了TryEnter(Object o,[int])的一个重载方法,该方法尝试获取o对象的独占权,当获取独占权失败时,将返回false。
但使用 lock 通常比直接使用 Monitor 更可取,一方面是因为 lock 更简洁,另一方面是因为 lock 确保了即使受保护的代码引发异常,也可以释放基础监视器。这是通过 finally 中调用Exit来实现的。事实上,lock 就是用 Monitor 类来实现的。下面两段代码是等效的:
Code lock (x) { DoSomething(); } 等效于 object obj = ( object )x; System.Threading.Monitor.Enter(obj); try { DoSomething(); } finally { System.Threading.Monitor.Exit(obj); }
关于用法,请参考下面的代码:
Code private static object m_monitorObject = new object (); [STAThread] static void Main( string [] args) { Thread thread = new Thread( new ThreadStart(Do)); thread.Name = " Thread1 " ; Thread thread2 = new Thread( new ThreadStart(Do)); thread2.Name = " Thread2 " ; thread.Start(); thread2.Start(); thread.Join(); thread2.Join(); Console.Read(); } static void Do() { if ( ! Monitor.TryEnter(m_monitorObject)) { Console.WriteLine( " Can't visit Object " + Thread.CurrentThread.Name); return ; } try { Monitor.Enter(m_monitorObject); Console.WriteLine( " Enter Monitor " + Thread.CurrentThread.Name); Thread.Sleep( 5000 ); } finally { Monitor.Exit(m_monitorObject); } }
当线程1获取了m_monitorObject对象独占权时,线程2尝试调用TryEnter(m_monitorObject),此时会由于无法获取独占权而返回false.