在进入锁的学习前来看看Thread的方法,之前一直对这个方法不了解,今天学习了下。在学习之前看两段代码吧:
1 static void Main(string[] args) 2 { 3 Thread thread = new Thread(new ThreadStart(myThread1)); 4 thread.Start(); 5 thread.Join(); //关键这一行 6 Console.WriteLine("主线程"); 7 Console.ReadKey(); 8 } 9 public static void myThread1() 10 { 11 Thread.Sleep(1000); 12 Console.WriteLine("1测试线程{0}",++count1); 13 }
先来看看效果再说话(左边截图为5行未被注释,右边为被注释):
在这之前,小弟一直不明白为什么加了上面第5行与不加第五行区别是什么,今天终于知道了,原来是线程之间原本并行执行通过使用Join()使其串行化,在这个例子里myThread1()被调用,而此方法存在一个线程阻塞,此时先打印“主线程”(上 右图);然而调用了Join()方法,使其原本并行化的线程串行化,所以主线程必须等待子线程执行完才能执行,此时先打印“1测试线程1”(上 左图)。<以上的串行与并行用词有点不严谨,主要事为了方便理解而已>
进入锁的学习
此句网上COPY来的:我们抛开.NET环境看线程同步,无非是执行两种操作:一是互斥/加锁,目的是保证临界区代码操作的“原子性”;另一种是信号灯操作,目的是保证多个线程按照一定顺序执行,如生产者线程要先于消费者线程执行。
暂时先脑海中留点锁的印象就好啦!下面介绍两个类:
Monitor类
通过查MSDN我们可以发现Monitor类一共有17个方法,但是这17个方法并不是都常用,下面我简单列举几个介绍并结合实例理解理解:
一、Enter()与Exit()
在我看来它目前最主要的功能是设置边界,使原本并行执行的线程顺序化,此句小弟断章取义,不对请指正
1 static void Main(string[] args) 2 { 3 for (int i = 0; i < 15; i++) 4 { 5 Thread thread = new Thread(new ThreadStart(myThread1)); 6 thread.Start(); 7 } 8 Console.WriteLine("主线程"); 9 Console.ReadKey(); 10 } 11 static object ob = new object(); 12 static int count1 = 0; 13 public static void myThread1() 14 { 15 Monitor.Enter(ob); //作用域开始 16 Thread.Sleep(10); 17 Console.WriteLine("1测试线程{0}", ++count1); 18 Monitor.Exit(ob); //作用域结束 19 }
看看效果再说话(左边截图为使用Enter()和Exit(),右边木有用):
看到区别木有,话说程序员都是聪明的班子,哈哈哈、、、、
二、Wait()与Pulse()
我们先来看看MSDN的官方介绍
Wait()——释放对象上的锁并阻止当前线程,直到它重新获取该锁
Pulse()——通知等待队列中的线程锁定对象状态的更改
简而言之,Wait()方法就是暂时释放资源锁,线程进入等待队列,此时其它线程获取资源锁;Pulse()方法则是唤醒等待队列中的线程,重新得到资源锁。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace WaitAndPulse { public class ClassFor { class Program { static void Main(string[] args) { for (int i = 0; i < 5; i++) { Thread thread = new Thread(new ThreadStart(myThread1)); thread.Start(); Thread thread1 = new Thread(new ThreadStart(myThread2)); thread1.Start(); } Console.WriteLine("主线程"); } static object ob = new object(); static int count1 = 0; static int count2 = 0; public static void myThread1() { Monitor.Enter(ob); Thread.Sleep(10); Console.WriteLine("1测试线程{0}", ++count1); Monitor.Wait(ob); Console.WriteLine("wait"); Monitor.Pulse(ob); Monitor.Exit(ob); } public static void myThread2() { Monitor.Enter(ob); Thread.Sleep(10); Console.WriteLine("2测试线程{0}", ++count2); Monitor.Wait(ob); Console.WriteLine("wait2"); Monitor.Pulse(ob); Monitor.Exit(ob); } } } }
运行结果如下图:
上面打印交替次数是没有规律的,每次都会有偏差
再总结几点:
1.Monitor.Pulse()调用后线程还是会执行下一行代码,不会执行另一个线程,除非再调用Monitor.Wait()让线程进入等待状态
2.只有锁的当前所有者可以使用 Pulse 向等待对象发出信号
3.当前拥有指定对象上的锁的线程调用此方法以便向队列中的下一个线程发出锁的信号。 接收到脉冲后,等待线程就被移动到就绪队列中。 在调用 Pulse 的线程释放锁后,就绪队列中的下一个线程(不一定是接收到脉冲的线程)将获得该锁(此句来自MSDN)
ReaderWriterLock类
说句实话,这个类我还真不太熟悉,简单介绍我知道的吧!
我们都知道我们日常代码中,绝大部分都是读取,少部分为写入,而我们前面学习的Monitor类在这里就有些不适合了,因此就出现了ReaderWriterLock这个类,它的好处就是起到,并实现了”写入串行“,”读取并行“的神奇效果。
不过这个类还不慎了解,此处留个坑,待填、、、
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace ProcessTest { class Program { //资源 static int theResource = 0; //读、写操作锁 static ReaderWriterLock rwl = new ReaderWriterLock(); static void Main(string[] args) { //分别创建2个读操作线程,2个写操作线程,并启动 Thread tr0 = new Thread(new ThreadStart(Read)); Thread tr1 = new Thread(new ThreadStart(Read)); Thread tr2 = new Thread(new ThreadStart(Write)); Thread tr3 = new Thread(new ThreadStart(Write)); tr0.Start(); tr1.Start(); tr2.Start(); tr3.Start(); //等待线程执行完毕 tr0.Join(); tr1.Join(); tr2.Join(); tr3.Join(); System.Console.ReadKey(); } //读数据 static void Read() { for (int i = 0; i < 3; i++) { try { //申请读操作锁,如果在1000ms内未获取读操作锁,则放弃 rwl.AcquireReaderLock(1000); Console.WriteLine("开始读取数据,theResource = {0}", theResource); Thread.Sleep(10); Console.WriteLine("读取数据结束,theResource = {0}", theResource); //释放读操作锁 rwl.ReleaseReaderLock(); } catch (ApplicationException) { //获取读操作锁失败的处理 } } } //写数据 static void Write() { for (int i = 0; i < 3; i++) { try { //申请写操作锁,如果在1000ms内未获取写操作锁,则放弃 rwl.AcquireWriterLock(1000); Console.WriteLine("开始写数据,theResource = {0}", theResource); //将theResource加1 theResource++; Thread.Sleep(100); Console.WriteLine("写数据结束,theResource = {0}", theResource); //释放写操作锁 rwl.ReleaseWriterLock(); } catch (ApplicationException) { //获取写操作锁失败 } } } } }
C#多线程:使用ReaderWriterLock类实现多用户读/单用户写同步
使用Monitor或Mutex进行同步控制的问题:由于独占访问模型不允许任何形式的并发访问,这样的效率总是不太高。许多时候,应用程序在访问资源时是进行读操作,写操作相对较少。为解决这一问题,C#提供了System.Threading.ReaderWriterLock类以适应多用户读/单用户写的场景。该类可实现以下功能:如果资源未被写操作锁定,那么任何线程都可对该资源进行读操作锁定,并且对读操作锁数量没有限制,即多个线程可同时对该资源进行读操作锁定,以读取数据。如果资源未被添加任何读或写操作锁,那么一个且仅有一个线程可对该资源添加写操作锁定,以写入数据。简单的讲就是:读操作锁是共享锁,允许多个线程同时读取数据;写操作锁是独占锁,同一时刻,仅允许一个线程进行写操作。
观察运行结果,我们很容易看出:读操作锁是共享锁,允许多个线程同时读取数据;写操作锁是独占锁,仅允许一个线程进行写操作。
如果一个线程在获取读操作锁后,进行读操作的途中,希望提升锁级别,将其变为写操作锁,可以调用ReaderWriterLock类的UpgradeToWriterLock(int timeOut)方法,该方法返回一个LockCookie值,该值保存了UpgradeToWriterLock方法调用前线程锁的状态。待写操作完成后,可调用DowngradeFromWriterLock(LockCookie lockcookie)方法,该方法根据传入的LockCookie参数值,将线程锁恢复到UpgradeToWriterLock方法调用前的状态。具体使用方法,大家可以查看MSDN以获取相关示例