GPS平台、网站建设、软件开发、系统运维,找森大网络科技!
http://cnsendnet.taobao.com
来自森大科技官方博客
http://www.cnsendblog.com/index.php/?p=405php
咱们在编程的时候,有时会使用多线程来解决问题,好比你的程序须要在后台处理一大堆数据,但还要使用户界面处于可操做状态;或者你的程序须要访问一些外部资源如数据库或网络文件等。这些状况你均可以建立一个子线程去处理,然而,多线程不可避免地会带来一个问题,就是线程同步的问题。若是这个问题处理很差,咱们就会获得一些非预期的结果。
在网上也看过一些关于线程同步的文章,其实线程同步有好几种方法,下面我就简单的作一下概括。
1、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.
如: react
public class A { private volatile int _i; public int I { get { return _i; } set { _i = value; } } }
但volatile并不能实现真正的同步,由于它的操做级别只停留在变量级别,而不是原子级别。若是是在单处理器系统中,是没有任何问题的,变量在主存中没有机会被其余人修改,由于只有一个处理器,这就叫做processor Self-Consistency。但在多处理器系统中,可能就会有问题。 每一个处理器都有本身的data cach,并且被更新的数据也不必定会当即写回到主存。因此可能会形成不一样步,但这种状况很难发生,由于cach的读写速度至关快,flush的频率也至关高,只有在压力测试的时候才有可能发生,并且概率很是很是小。
2、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一个字符串
3、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 );
Output:
4、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 类来实现的。下面两段代码是等效的:网络
lock (x) { DoSomething(); } 等效于 object obj = ( object )x; System.Threading.Monitor.Enter(obj); try { DoSomething(); } finally { System.Threading.Monitor.Exit(obj); }
关于用法,请参考下面的代码:多线程
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,输出信息以下:
另外,Monitor还提供了三个静态方法Monitor.Pulse(Object o),Monitor.PulseAll(Object o)和Monitor.Wait(Object o ) ,用来实现一种唤醒机制的同步。关于这三个方法的用法,能够参考MSDN,这里就不详述了。
5、Mutex
在使用上,Mutex与上述的Monitor比较接近,不过Mutex不具有Wait,Pulse,PulseAll的功能,所以,咱们不能使用Mutex实现相似的唤醒的功能。不过Mutex有一个比较大的特色,Mutex是跨进程的,所以咱们能够在同一台机器甚至远程的机器上的多个进程上使用同一个互斥体。尽管Mutex也能够实现进程内的线程同步,并且功能也更强大,但这种状况下,仍是推荐使用Monitor,由于Mutex类是win32封装的,因此它所须要的互操做转换更耗资源。
6、ReaderWriterLock
在考虑资源访问的时候,惯性上咱们会对资源实施lock机制,可是在某些状况下,咱们仅仅须要读取资源的数据,而不是修改资源的数据,在这种状况下获取资源的独占权无疑会影响运行效率,所以.Net提供了一种机制,使用ReaderWriterLock进行资源访问时,若是在某一时刻资源并无获取写的独占权,那么能够得到多个读的访问权,单个写入的独占权,若是某一时刻已经获取了写入的独占权,那么其它读取的访问权必须进行等待,参考如下代码:运维
private static ReaderWriterLock m_readerWriterLock = new ReaderWriterLock(); private static int m_int = 0; [STAThread] static void Main(string[] args) { Thread readThread = new Thread(new ThreadStart(Read)); readThread.Name = "ReadThread1"; Thread readThread2 = new Thread(new ThreadStart(Read)); readThread2.Name = "ReadThread2"; Thread writeThread = new Thread(new ThreadStart(Writer)); writeThread.Name = "WriterThread"; readThread.Start(); readThread2.Start(); writeThread.Start(); readThread.Join(); readThread2.Join(); writeThread.Join(); Console.ReadLine(); } private static void Read() { while (true) { Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " AcquireReaderLock"); m_readerWriterLock.AcquireReaderLock(10000); Console.WriteLine(String.Format("ThreadName : {0} m_int : {1}", Thread.CurrentThread.Name, m_int)); m_readerWriterLock.ReleaseReaderLock(); } } private static void Writer() { while (true) { Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " AcquireWriterLock"); m_readerWriterLock.AcquireWriterLock(1000); Interlocked.Increment(ref m_int); Thread.Sleep(5000); m_readerWriterLock.ReleaseWriterLock(); Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " ReleaseWriterLock"); } }
在程序中,咱们启动两个线程获取m_int的读取访问权,使用一个线程获取m_int的写入独占权,执行代码后,输出以下:
能够看到,当WriterThread获取到写入独占权后,任何其它读取的线程都必须等待,直到WriterThread释放掉写入独占权后,才能获取到数据的访问权,应该注意的是,上述打印信息很明显显示出,能够多个线程同时获取数据的读取权,这从ReadThread1和ReadThread2的信息交互输出能够看出。
7、SynchronizationAttribute
当咱们肯定某个类的实例在同一时刻只能被一个线程访问时,咱们能够直接将类标识成Synchronization的,这样,CLR会自动对这个类实施同步机制,实际上,这里面涉及到同步域的概念,当类按以下设计时,咱们能够确保类的实例没法被多个线程同时访问
1). 在类的声明中,添加System.Runtime.Remoting.Contexts.SynchronizationAttribute属性。
2). 继承至System.ContextBoundObject
须要注意的是,要实现上述机制,类必须继承至System.ContextBoundObject,换句话说,类必须是上下文绑定的。
一个示范类代码以下:ide
[System.Runtime.Remoting.Contexts.Synchronization] public class SynchronizedClass : System.ContextBoundObject { }
8、MethodImplAttribute
若是临界区是跨越整个方法的,也就是说,整个方法内部的代码都须要上锁的话,使用MethodImplAttribute属性会更简单一些。这样就不用在方法内部加锁了,只须要在方法上面加上 [MethodImpl(MethodImplOptions.Synchronized)] 就能够了,MehthodImpl和MethodImplOptions都在命名空间System.Runtime.CompilerServices 里面。但要注意这个属性会使整个方法加锁,直到方法返回,才释放锁。所以,使用上不太灵活。若是要提早释放锁,则应该使用Monitor或lock。咱们来看一个例子:函数
[MethodImpl(MethodImplOptions.Synchronized)] public void DoSomeWorkSync() { Console.WriteLine( " DoSomeWorkSync() -- Lock held by Thread " + Thread.CurrentThread.GetHashCode()); Thread.Sleep( 1000 ); Console.WriteLine( " DoSomeWorkSync() -- Lock released by Thread " + Thread.CurrentThread.GetHashCode()); } public void DoSomeWorkNoSync() { Console.WriteLine( " DoSomeWorkNoSync() -- Entered Thread is " + Thread.CurrentThread.GetHashCode()); Thread.Sleep( 1000 ); Console.WriteLine( " DoSomeWorkNoSync() -- Leaving Thread is " + Thread.CurrentThread.GetHashCode()); } [STAThread] static void Main( string [] args) { MethodImplAttr testObj = new MethodImplAttr(); Thread t1 = new Thread( new ThreadStart(testObj.DoSomeWorkNoSync)); Thread t2 = new Thread( new ThreadStart(testObj.DoSomeWorkNoSync)); t1.Start(); t2.Start(); Thread t3 = new Thread( new ThreadStart(testObj.DoSomeWorkSync)); Thread t4 = new Thread( new ThreadStart(testObj.DoSomeWorkSync)); t3.Start(); t4.Start(); Console.ReadLine(); }
这里,咱们有两个方法,咱们能够对比一下,一个是加了属性MethodImpl的DoSomeWorkSync(),一个是没加的DoSomeWorkNoSync()。在方法中Sleep(1000)是为了在第一个线程还在方法中时,第二个线程可以有足够的时间进来。对每一个方法分别起了两个线程,咱们先来看一下结果:
能够看出,对于线程1和2,也就是调用没有加属性的方法的线程,当线程2进入方法后,尚未离开,线程1有进来了,这就是说,方法没有同步。咱们再来看看线程3和4,当线程3进来后,方法被锁,直到线程3释放了锁之后,线程4才进来。
9、同步事件和等待句柄
用lock和Monitor能够很好地起到线程同步的做用,但它们没法实现线程之间传递事件。若是要实现线程同步的同时,线程之间还要有交互,就要用到同步事件。同步事件是有两个状态(终止和非终止)的对象,它能够用来激活和挂起线程。
同步事件有两种:AutoResetEvent和 ManualResetEvent。它们之间惟一不一样的地方就是在激活线程以后,状态是否自动由终止变为非终止。AutoResetEvent自动变为非终止,就是说一个AutoResetEvent只能激活一个线程。而ManualResetEvent要等到它的Reset方法被调用,状态才变为非终止,在这以前,ManualResetEvent能够激活任意多个线程。
能够调用WaitOne、WaitAny或WaitAll来使线程等待事件。它们之间的区别能够查看MSDN。当调用事件的 Set方法时,事件将变为终止状态,等待的线程被唤醒。
来看一个例子,这个例子是MSDN上的。由于事件只用于一个线程的激活,因此使用 AutoResetEvent 或 ManualResetEvent 类均可以。测试
static AutoResetEvent autoEvent; static void DoWork() { Console.WriteLine(" worker thread started, now waiting on event "); autoEvent.WaitOne(); Console.WriteLine(" worker thread reactivated, now exiting "); } [STAThread] static void Main(string[] args) { autoEvent = new AutoResetEvent(false); Console.WriteLine("main thread starting worker thread "); Thread t = new Thread(new ThreadStart(DoWork)); t.Start(); Console.WriteLine("main thrad sleeping for 1 second "); Thread.Sleep(1000); Console.WriteLine("main thread signaling worker thread "); autoEvent.Set(); Console.ReadLine(); }
咱们先来看一下输出:
在主函数中,首先建立一个AutoResetEvent的实例,参数false表示初始状态为非终止,若是是true的话,初始状态则为终止。而后建立并启动一个子线程,在子线程中,经过调用AutoResetEvent的WaitOne方法,使子线程等待指定事件的发生。而后主线程等待一秒后,调用AutoResetEvent的Set方法,使状态由非终止变为终止,从新激活子线程。
GPS平台、网站建设、软件开发、系统运维,找森大网络科技!
http://cnsendnet.taobao.com
来自森大科技官方博客
http://www.cnsendblog.com/index.php/?p=405