C#线程从陌生到熟悉(4)

互斥与同步
今天我们来看互斥与同步,互斥与同步一直线程操作方面的至关重要的一部份.首先来看下他们之间的概念.
互斥和同步都是在并发环境下编程特有的问题.并发是指宏观上多个线程同时进行而某一时刻只有一个线程运行,即宏观并行微观串行.大家应该都知道时间片的概念,上面的话应该很容易理解的.并发环境下,不能有两个以上(包括两个)某类特殊的资源,就叫互斥.这类特殊资源也被称为临界资源,例如字符串缓冲区,文件,实例对象等.同步是指多个线程互相通信,互相等待从而使进程按照一定顺序往前推进.其实特殊的资源应该就是经常用到的变量与对象只不过在某一时刻只能由一个线程操作他.
1.互斥
.NET公共运行库提供了几种方法实现对临界资源的互斥访问.
首先看Monitor这个类

[ComVisible(true)]
public static class Monitor
{
[SecuritySafeCritical]
public static void Enter(object obj);
[SecuritySafeCritical]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public static void Exit(object obj);
[SecuritySafeCritical]
public static void Pulse(object obj);
public static bool TryEnter(object obj);
[SecuritySafeCritical]
public static bool Wait(object obj);

.....
}

这几个方法简单介绍,Enter获取对象锁(参数obj),Exit释放对象锁,Pulse通知等待队列中的线程锁定对象状态的更改,TryEnter尝试获取对象锁,Wait释放对象上的锁并阻止当前线程,直到它重新获取该锁。
下面来看个例子:

 1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading;
6
7 namespace ConsoleApplication8
8 {
9 class Program
10 {
11 int x = 100;
12 static void Main(string[] args)
13 {
14 Program p = new Program();
15 Thread t1 = new Thread(p.MyWork1);
16 Thread t2 = new Thread(p.MyWork2);
17 t1.Start();
18 t2.Start();
19 }
20 void MyWork1()
21 {
22 try
23 {
24 Monitor.Enter(this);
25 while (true)
26 {
27
28 if (x < 1) { break; }
29 x--;
30 Console.WriteLine("调用MyWork1()");
31 }
32 }
33 finally
34 {
35 Monitor.Exit(this);
36 }
37 }
38 void MyWork2()
39 {
40
41 try
42 {
43 Monitor.Enter(this);
44 while (true)
45 {
46
47 if (x < 1) { break; }
48 x = x - 2;
49 Console.WriteLine("调用MyWork2()");
50
51 }
52 }
53 finally
54 {
55 Monitor.Exit(this);
56 }
57 }
58 }
59 }

输出结果为

结果全为MyWork1的调用,如果将 Monitor.Enter(this)和Monitor.Exit(this)注释起来的话,在运行的话,就会出现MyWork1和Mywork2都在运行了.还有可以将MyWork1中的x<1改成10那运行结果为

可见MyWork1先运行了,当他释放完锁之后MyWork2获取到对象所之后才运行.从这个例子我们可以看出 Monitor.Enter(this)和Monitor.Exit(this)的用法.按照我的理解:Monitor.Enter(this)会一直等待,直到获到对象锁.对于这个猜测,我们可以在MyWork2的 Monitor.Enter(this)加一句输出语句.可以看这个输出结果会在控制台上.

将上面的例子稍加更改,来重点说下TryEnter,Wait,Pulse这三函数的用法及相关解释.

先看代码

 1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading;
6
7 namespace ConsoleApplication8
8 {
9 class Program
10 {
11 int x = 100;
12 static void Main(string[] args)
13 {
14 Program p = new Program();
15 Thread t1 = new Thread(p.MyWork1);
16 Thread t2 = new Thread(p.MyWork2);
17 t1.Start();
18 // Thread.Sleep(2000);
19 t2.Start();
20 }
21 void MyWork1()
22 {
23 try
24 {
25 Monitor.Enter(this);
26 // Monitor.Pulse(this);
27 Monitor.Wait(this);
28 while (true)
29 {
30
31 if (x < 1) { break; }
32 x--;
33 Console.WriteLine("调用MyWork1()");
34 }
35 }
36 finally
37 {
38 try
39 {
40 Monitor.Exit(this);
41 }
42 catch (Exception e)
43 {
44 Console.WriteLine("MyWork1异常退出,信息为");
45 Console.WriteLine(e.Message);
46 }
47 }
48 }
49 void MyWork2()
50 {
51
52 try
53 {
54 //Monitor.Enter(this)
55 if (Monitor.TryEnter(this))
56 {
57 Monitor.Pulse(this);
58 while (true)
59 {
60
61 if (x < 10) { break; }
62 x = x - 2;
63 Console.WriteLine("调用MyWork2()");
64
65 }
66 }
67 }
68 finally
69 {
70 try
71 {
72
73 Monitor.Exit(this);
74
75 }
76 catch (Exception e)
77 {
78 Console.WriteLine("MyWork2异常退出,信息为");
79 Console.WriteLine(e.Message);
80 }
81 }
82 }
83 }
84 }


运行结果如下图

程序的执行流程为,首先MyWork1获得对象锁,然后调用Wait方法释放锁,阻止当前线程,直到再次获得对象锁为止.MyWork2此时运行,调用TryEnter这个方法,注意了,这个方法和Enter有很大的区别的,他会立即返回结果.不像Enter会一直在那等.因为之前对象锁已经释放,所以返回真.如果将Wait方法注释的话,MyWork2里的TryEnter方法将立即返回假,程序异常结束.接着调用Pulse方法,通知其他线程锁定对象状态的更改,当X<10对象锁释放,而一直处于等待MyWork1将再次获得对象锁.执行相应的动作.这里我要说下Pulse这个方法,如果MyWork2将这个方法注释,那么MyWork1这个线程将一直处于等待状态.一直获不到对象锁的.

通过这两个例子,应该对Monitor这个类比较了解了.

还有重要的东西,如果是类的静态方法或属性,此时就要锁定该类型了,这个地方要注意的,在这我也给各例子:

结果这里就不给了;

 1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading;
6
7 namespace ConsoleApplication9
8 {
9 class Program
10 {
11 static void Main(string[] args)
12 {
13 Thread t1 = new Thread(MyWork1);
14 Thread t2 = new Thread(MyWork2);
15 t1.Start();
16 t2.Start();
17 }
18 static void MyWork1()
19 {
20 Monitor.Enter(typeof(Program));
21 for (int i = 1; i < 20; i++)
22 {
23 Console.WriteLine("MyWork1()");
24 Thread.Sleep(10);
25 }
26 Monitor.Exit(typeof(Program));
27 }
28 static void MyWork2()
29 {
30 Monitor.Enter(typeof(Program));
31 for (int i = 1; i < 20; i++)
32 {
33 Console.WriteLine("MyWork2()");
34 Thread.Sleep(10);
35 }
36 Monitor.Exit(typeof(Program));
37 }
38 }
39 }

下面看.Net比较简单操作同步的方法

 如果临界区跨越整个方法,则可以通过将using System.Runtime.CompilerServices的MethodImplAttribute放置在方法并在MethodImplAttribute德构造函数中制定MethodImplOptions.Synchronized值来实现上述锁定功能.对于上面的例子,可作如下更改

   [MethodImpl(MethodImplOptions.Synchronized)]
   static void MyWork1()
   {
            //Monitor.Enter(typeof(Program));
            for (int i = 1; i < 20; i++)
            {
                Console.WriteLine("MyWork1()");
                Thread.Sleep(10);
            }
           // Monitor.Exit(typeof(Program));
   }

MyWork2也一样,运行结果一样.

lock关键字,lock(expression)必须是引用类型.

可将上面的例子作如下更改  

 static   void MyWork1()
{
       lock (typeof(Program))
      {
                for (int i = 1; i < 20; i++)
                {
                    Console.WriteLine("MyWork1()");
                    Thread.Sleep(10);
                }
      }
 }

这里注意下MyWork1是静态方法,所以锁定类型,大多时候我们都是用lock(this);

ReaderWriterLock这个类有着重多的优点,比如开销低,支持超时等等.看下这个类的定义以及常用的方法

[ComVisible(true)]
public sealed class ReaderWriterLock : CriticalFinalizerObject
{
  public void AcquireReaderLock(int millisecondsTimeout);
   public void AcquireWriterLock(int millisecondsTimeout);
   public LockCookie ReleaseLock();
   public void ReleaseReaderLock();
   public void ReleaseWriterLock();
......
}

请看下面的例子

 1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading;
6
7 namespace ConsoleApplication10
8 {
9 class Program
10 {
11 int value = 100;
12 ReaderWriterLock rwl = new ReaderWriterLock();
13 static void Main(string[] args)
14 {
15 Program p = new Program();
16 Thread tr1 = new Thread(p.Read);
17 Thread tw1 = new Thread(p.Write);
18 tr1.Start();
19 tw1.Start();
20
21
22 }
23 void Read()
24 {
25 rwl.AcquireReaderLock(Timeout.Infinite);
26 Console.WriteLine("Read开始读");
27 Thread.Sleep(10);
28 Console.WriteLine("Read读出的Value值为{0}", value);
29 Console.WriteLine("Read结束读");
30 rwl.ReleaseReaderLock();
31 }
32 void Write()
33 {
34 rwl.AcquireWriterLock(Timeout.Infinite);
35 Console.WriteLine("Write开始写");
36 Thread.Sleep(10);
37 value++;
38 Console.WriteLine("Write写完后的Value值为{0}", value);
39 Console.WriteLine("Write结束写");
40 rwl.ReleaseReaderLock();
41 }
42 }
43 }

结果为

大家可以看到读的时候锁定对象,无法写入,换两个线程的开始位置,就可以得到写的时候锁定对象,读的操作无法读取.

这里有两个方法有点意思的public LockCookie UpgradeToWriterLock(int millisecondsTimeout)和public LockCookie UpgradeToWriterLock(TimeSpan timeout)这个方法是读写线程锁的转换.大家可以自己尝试下.

好了,今天到这.关于互斥还有些内容,下次再讨论!

原文地址:https://www.cnblogs.com/enuo/p/2281841.html