线程同步
上一遍线程基本概念最后一个例子中,两个线程通过同样的方法操作一个静态变量,多次运行结果一定不会相同,分析原因是两个线程在同一时刻更新nums变量,解决这个问题的方法是在同一时刻只能一个线程能更新nums值,也就是当一个线程操作nums时候另外一个或者多个线程必须等待,直到当前线程结束结变量的操作,这样就能够安全的更新变量值确保变量值安全更新,先明白两具概念。
1.临界资源,每次只允许一个线程访问的资源,上节示例是操作nums运行减法操作的方法就属于临界资源。
2.线程同步,线程间相互协调同时或者顺序运行相关代码模块。
公共语言基础结构(Common Language Infrastructure)提供了3种策略实现访问实例、静态方法和实例字段,这3种方法是:同步代码区、手控同步、同步上下文。
一、同步代码区。
要同步的代码区是方法中重要的代码段,它们可以改变对象的状态,或者更新另一个资源,.net提供了两个常用的类可以进行相应的操作,Monitor类和ReaderWriterLock类。
1、Monitor类进行同步操作
Monitor类的成员是下表(来源于微软网站)中的方法。
名称 | 说明 | |
---|---|---|
![]() ![]() ![]() |
Enter | 在指定对象上获取排他锁。 |
![]() ![]() |
Equals | 已重载。 确定两个 Object 实例是否相等。 (从 Object 继承。) |
![]() ![]() ![]() |
Exit | 释放指定对象上的排他锁。 |
![]() ![]() |
GetHashCode | 用作特定类型的哈希函数。GetHashCode 适合在哈希算法和数据结构(如哈希表)中使用。 (从 Object 继承。) |
![]() ![]() |
GetType | 获取当前实例的 Type。 (从 Object 继承。) |
![]() ![]() |
Pulse | 通知等待队列中的线程锁定对象状态的更改。 |
![]() ![]() |
PulseAll | 通知所有的等待线程对象状态的更改。 |
![]() ![]() ![]() |
ReferenceEquals | 确定指定的 Object 实例是否是相同的实例。 (从 Object 继承。) |
![]() ![]() |
ToString | 返回表示当前 Object 的 String。 (从 Object 继承。) |
![]() ![]() ![]() |
TryEnter | 已重载。 试图获取指定对象的排他锁。 |
![]() ![]() |
Wait | 已重载。 释放对象上的锁并阻止当前线程,直到它重新获取该锁。 |
Monitor类用于同步代码区,其方式是使用Monitor.Enter()方法警告一个锁,然后在代码区上可以使用Wait()、Pluse()、PluseAll()方法进行线程间同步,最后代码区执行结束,调用Monitor.Exit()方法释放锁。
Monitor类的方法是静态方法,先看下Enter()方法和Exit()方法示例:

using System; using System.Collections.Generic; using System.Text; using System.Threading; //注意此处添加引用。 namespace ThreadDemoCreateThead { class Program { //声明静态变量。 static int nums = 1000; //声明一个静态锁 static object locker = new object(); static void Main(string[] args) { //创建线程1 Thread thread1 = new Thread(PlusNum); //创建线程1 Thread thread2 = new Thread(PlusNum); //给线程命名 thread1.Name = "线程1"; thread2.Name = "线程2"; //启动线程 thread1.Start(); thread2.Start(); Console.ReadKey(); } // /// <summary> /// 减法运算 /// </summary> /// <param name="num"></param> static void PlusNum() { Monitor.Enter(locker); Console.WriteLine( Thread.CurrentThread.Name+":Num当前值是:" + nums); Thread.Sleep(100); nums -= 1000; Console.WriteLine(Thread.CurrentThread.Name + ":Num操作减1000后是:" + nums); Monitor.Exit(locker); Console.WriteLine(Thread.CurrentThread.Name + ":锁已释放,执行其他操作。"); Thread.Sleep(1000); Console.WriteLine(Thread.CurrentThread.Name + ":方法结束"); // } } }
结果如下图:
Wait()和Pulse()机制,Wait()和Pulse()机制用于线程间的交互,当在一个对象上执行Wait()时,正在访问该对象的线程会等待状态,直到它得到一个唤醒的信号,而Pulse()方法就是用来发出这个唤醒信号的方法 。看下面示例:

using System; using System.Collections.Generic; using System.Text; using System.Threading; //注意此处添加引用。 namespace ThreadDemoCreateThead { /// <summary> /// 定义一个锁类 /// </summary> public class Locker { } /// <summary> /// 定义示例类一 /// </summary> public class WaitPulse1 { public WaitPulse1(Locker newLocker) { locker = newLocker; } private Locker locker; private int result = 0; public void WaitPulseMethod() { Monitor.Enter(locker); Console.WriteLine(Thread.CurrentThread.Name+":我获得了锁 locker"); for (int i = 0; i < 2; i++) { Console.WriteLine(Thread.CurrentThread.Name + ":我调用了Wait()方法,我等待别人唤醒我。"); Monitor.Wait(locker); Console.WriteLine(Thread.CurrentThread.Name + ":终于有人唤醒我了,我继续执行"); result++; Console.WriteLine(Thread.CurrentThread.Name + ": Result=" + result ); Monitor.Pulse(locker); Console.WriteLine(Thread.CurrentThread.Name + ":做线程不能太贪,我拥有别人也能用,谁在等待locker锁可以用了。我已经调用了Pulse()方法了"); } Console.WriteLine(Thread.CurrentThread.Name + ": Method1 结束"); Monitor.Exit(locker); } } /// <summary> /// 定义示例二 /// </summary> public class WaitPulse2 { public WaitPulse2(Locker newLocker) { locker = newLocker; } private Locker locker; private int result = 0; public void WaitPulseMethod() { Monitor.Enter(locker); Console.WriteLine(Thread.CurrentThread.Name + ":我获得了锁 locker"); for (int i = 0; i < 2; i++) { Console.WriteLine(Thread.CurrentThread.Name + ":做线程不能太贪,我拥有别人也能用,谁在等待locker锁可以用了。我已经调用了Pulse()方法了"); Monitor.Pulse(locker); result++; Console.WriteLine(Thread.CurrentThread.Name + ": Result=" +result ); Console.WriteLine(Thread.CurrentThread.Name + ":我调用了Wait()方法,我等待别人唤醒我。"); Monitor.Wait(locker); } Console.WriteLine(Thread.CurrentThread.Name + ": Method2 结束"); Monitor.Exit(locker); } } class Program { static void Main(string[] args) { Locker locker = new Locker(); WaitPulse1 example1 = new WaitPulse1(locker); WaitPulse2 example2 = new WaitPulse2(locker); //创建线程1 Thread thread1 = new Thread(example1.WaitPulseMethod); //创建线程1 Thread thread2 = new Thread(example2.WaitPulseMethod); //给线程命名 thread1.Name = "线程1"; thread2.Name = "线程2"; //启动线程 thread1.Start(); Thread.Sleep(100); thread2.Start(); Console.ReadKey(); } } }
运行结果如下图:
示例代码执行流程说明:在Main方法中创建一个Locker实例locker,然后使用该对象初始化WaitPulse1和WaitPulse2对象,然后把这两个对象的方法做为两个委托创建两个线程,并给线程命名线程1、线程2。然后启动线程一,线程一通过Monitor.Enter(locker)获得锁locker,继续执行,在进入for循环时线程执行Monitor.Wait()方法临时释放locker锁,此时线程一由于临时释放了locker锁等待别的线程唤醒他。线程一释放locker锁后,线程二通过Monitor.Enter(locker)获得锁locker,线程二继续执行,在进入for循环后就调用Monitor.Pulse()方法给线程一一个信号,线程二继续执行,然后调用Monitor.Wait()方法临时释放locker锁,线程二等待别的线程发送信号唤醒他。此时线程一由于收到了线程二刚刚发出的Pulse()信号而醒来可以继续执行下面的代码,执行过后线程一调用Monitor.Pulse()发出一个locker可用信号,激活线程二然后线程一进入下一个循环,线程二获得locker。两个循环结束线程分别释放locker对象。 注意Monitor.Wait()和Monitor.Pulse()方法只能在Monitor.Enter(locker)和Monitor.Exit(locker)代码间使用.
Monitor.TryEnter()方法和Monitor.Enter()相似,只是TryEnter()方法尝试获得锁如果获得返回True否则返回False,无论TryEnter()方法是否获得锁,线程都会继续执行。
lock关键字可以看作是Monitor的的一个替代,下面两部分代码是等价的:
Monitor.Enter(obj)
......
Monitor.Exit(obj)
和lock(obj){......}
lock 示例:

using System; using System.Collections.Generic; using System.Text; using System.Threading; //注意此处添加引用。 namespace ThreadDemoCreateThead { /// <summary> /// 定义一个锁类 /// </summary> public class Locker { } /// <summary> /// 定义示例 /// </summary> public class WaitPulse2 { public WaitPulse2(Locker newLocker) { locker = newLocker; } private Locker locker; private int result = 0; public void WaitPulseMethod() { //Monitor.Enter(locker); lock (locker) { Console.WriteLine(Thread.CurrentThread.Name + ":我获得了锁 locker"); for (int i = 0; i < 2; i++) { Console.WriteLine(Thread.CurrentThread.Name + ": Resule="+result++); } Console.WriteLine(Thread.CurrentThread.Name + ": Method2 结束"); } //Monitor.Exit(locker); } } class Program { static void Main(string[] args) { Locker locker = new Locker(); WaitPulse1 example1 = new WaitPulse1(locker); WaitPulse2 example2 = new WaitPulse2(locker); //创建线程1 Thread thread1 = new Thread(example2.WaitPulseMethod); //创建线程1 Thread thread2 = new Thread(example2.WaitPulseMethod); //给线程命名 thread1.Name = "线程1"; thread2.Name = "线程2"; //启动线程 thread1.Start(); Thread.Sleep(100); thread2.Start(); Console.ReadKey(); } } }
运行结果如图:
注意:lock(obj),obj对象一般为private类型。
2.ReaderWriterLocker类
ReaderWriterLock类定义了实现单写程序和多读程序的语义的锁,这个类主要用于文件操作,即多个线程读取文件,但是只能用一个线程来更新文件。ReaderWriterLock类中4个主要的方法是:
AcquireReaderLock()该方法获得一个读程序锁,超时值使用整数或TimeSpan。
AcquireWriterLock()该方法获得一个写程序倘,超时值使用整数或TimeSpan.
ReleaseReaderLock()释放读程序锁。
ReleaseWriterLock()释放写程序锁。
使用ReaderWriterLock类时,任意数量的线程都可以同时安全地读取数据,只有当线程更新时数据才被锁定,读写锁关系是这样的:只有在写线程没有占用锁时,读线程才能获得锁;只有在读线程和写线程没有占用锁时候写线程才能获得锁。
请看示例:

using System; using System.Collections.Generic; using System.Text; using System.Threading; //注意此处添加引用。 namespace ThreadDemoCreateThead { /// <summary> /// 定义读写锁类 /// </summary> public class ReadWriteLockDemo { private ReaderWriterLock readerWriterLock; private int x; private int y; public ReadWriteLockDemo() { readerWriterLock = new ReaderWriterLock(); } public void Write(int nums1, int nums2) { readerWriterLock.AcquireWriterLock(Timeout.Infinite); Console.WriteLine("获得写锁"); try { x = nums1; y = nums2; Console.WriteLine(string.Format("获得写锁后,改变x={0},y={1}",x,y)); } finally { readerWriterLock.ReleaseWriterLock(); Console.WriteLine("释放写锁"); } } public void Read(ref int xValue, ref int yValue) { readerWriterLock.AcquireReaderLock(Timeout.Infinite); Console.WriteLine(Thread.CurrentThread.Name+ "获得读锁"); try { xValue = x; yValue = y; } finally { readerWriterLock.ReleaseReaderLock(); Console.WriteLine(Thread.CurrentThread.Name + "释放读锁"); } } } class Program { static ReadWriteLockDemo demo1 = new ReadWriteLockDemo(); static void Main(string[] args) { Thread writeLock1 = new Thread(Write); Thread ReadLock1 = new Thread(Read); Thread ReadLock2 = new Thread(Read); writeLock1.Start(); ReadLock1.Start(); ReadLock2.Start(); Console.ReadKey(); } private static void Write() { int a = 10; int b = 20; for (int i = 0; i < 3; i++) { demo1.Write(a++, b++); Thread.Sleep(1000); } } private static void Read() { int a = 5; int b = 6; for (int i = 0; i < 2; i++) { demo1.Read(ref a, ref b); Console.WriteLine("读取X值:{0},Y:{1}",a,b); } } } }
运行结果如下图:
看上图会发现程序运行结果和代码示例前分析的一样,先获得读锁,读取后释放读锁,只有两个读锁都释放后写程序才能写进行写操作,写操作结束后读程序获得读锁继续进行操作。
二、同步上下文。上下文就是把类似的对象组合在一起,可以使用 SynchronizationAttribute 为 ContextBoundObject 对象启用简单的自动同步,将该特性应用于某个对象(对象要继承自ContextBoundObject)时,在共享该属性实例的所有上下文中只能执行一个线程。这是通过提供为各个上下文截获并序列化传入调用的接收器来实现的。
示例:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Runtime.Remoting.Contexts; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { Account account = new Account(); Thread thread1 = new Thread(new ParameterizedThreadStart(account.WithDraw)); thread1.Start(1000); Thread thread2 = new Thread(new ParameterizedThreadStart(account.WithDraw)); thread2.Start(1000); Console.ReadKey(); } } [Synchronization()] public class Account:ContextBoundObject { private int balance = 2000; public int Balance { get { return balance; } } public void WithDraw(object target) { Console.WriteLine(Thread.CurrentThread.GetHashCode()+":开始取钱"); int tempNum = int.Parse(target.ToString()); if (balance >= tempNum) { balance -= tempNum; } Console.WriteLine(Thread.CurrentThread.GetHashCode() + ":结束取钱"); } } }
运行结果如下图:
下篇写最重要的手控同步。