C#线程从陌生到熟悉(5)
互斥体Mutex
Mutex是同步基元,他只向一个线程授予队共享资源的独占访问权.如果线程获取了互斥体,则要获取该互斥体的第二个线程将被挂起,直到第一个线程释放互斥体.
首先来看Mutex类的定义.
1 public sealed class Mutex : WaitHandle
2 {
3 public static Mutex OpenExisting(string name);
4 public void ReleaseMutex();
5 public MutexSecurity GetAccessControl();
6 public void SetAccessControl(MutexSecurity mutexSecurity);
7 ...
8 }
我们主要看看这四个方法,OpenExisting打开互斥体,注意是一个静态方法.ReleaseMutex释放互斥体.MutexSecurity表是对互斥体的访问权限. SetAccessControl设置已命名的系统互斥体的访问控制安全性.
这个类声明为sealed ,表示不能继承.有一个比较重要的是,他是从WaitHandle继承而来,WaitHandle这个类应该比较熟悉了,前面已经写过了.主要是这个类的Wait的方法.下面我们来看个简单的例子:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading;
6 using System.Security.AccessControl;
7
8 namespace ConsoleApplication11
9 {
10 class Program
11 {
12 Mutex m = new Mutex(false, "myMutex");
13 int val = 100;
14 static void Main(string[] args)
15 {
16 Program p = new Program();
17 Thread t1 = new Thread(p.Write);
18 Thread t2 = new Thread(p.Read);
19 t1.Start();
20 t2.Start();
21 Thread.Sleep(3000);
22 Mutex m1 = Mutex.OpenExisting("myMutex");
23 m1.WaitOne();
24 Console.WriteLine("结束!");
25 m1.ReleaseMutex();
26
27 }
28 public void Write()
29 {
30
31 m.WaitOne();
32 Thread.Sleep(2000);
33 try
34 {
35 val += 1;
36 Console.WriteLine("写val的值为{0}", val);
37
38 }
39 finally
40 {
41 m.ReleaseMutex();
42 }
43 }
44 public void Read()
45 {
46 m.WaitOne();
47 Thread.Sleep(1000);
48 try
49 {
50 Console.WriteLine("读val的值为{0}", val);
51 }
52 finally
53 {
54 m.ReleaseMutex();
55 }
56 }
57
58
59 }
60 }
运行结果为
Write方法首先获得Mutex对象,即时运行Thread.Sleep(2000),但是Read方法需要Mutex对象,现在没有释放,所以首先运行Write方法,然后运行Read这个方法.最后结束.
Interlocked
Interlocked这个类为多个类共享的变量提供原子操作.首先看看他的定义:
1 public static class Interlocked
2 {
3 public static int CompareExchange(ref int location1, int value, int comparand);
4 public static int Decrement(ref int location);
5 public static int Exchange(ref int location1, int value);
6 public static int Increment(ref int location);
7 ......
8 }
Exchange函数自动交换指定变量的值.CompareExchange比较两个值以及根据比较的结果将第三个存储在一个变量中.Increment和Decrement函数将递增和递减变量与检查结果值得操作组合起来.请看下面的例子:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading;
6
7 namespace ConsoleApplication12
8 {
9 class Program
10 {
11 static int AsyncOps = 6;
12 static AutoResetEvent asyncOpsAreDone = new AutoResetEvent(false);
13 static Book book=new Book();
14 static void Main(string[] args)
15 {
16 for (int i = 0; i < 6; i++)
17 {
18 ThreadPool.QueueUserWorkItem(new WaitCallback(ChangeBook), i);
19
20 }
21 asyncOpsAreDone.WaitOne();
22 Console.WriteLine("操作结束!");
23 }
24 static void ChangeBook(Object state)
25 {
26 int threadnum = (Int32)state;
27 if (threadnum % 2 != 0)
28 {
29 book.Read(threadnum);
30 }
31 else
32 {
33 book.Write(threadnum);
34 }
35 if (Interlocked.Decrement(ref AsyncOps) == 0)
36 {
37 asyncOpsAreDone.Set();
38 }
39 }
40 }
41
42
43 public class Book
44 {
45 public void Write(int i)
46 {
47 Console.WriteLine("线程{0}读书",i);
48 }
49 public void Read(int i)
50 {
51 Console.WriteLine("线程{0}写书",i);
52 }
53 }
54 }
结果为
结果可能是变化的.线程运行顺序不一定相同.AutoRestEvent前面已经写过他的介绍了。首先启动6个字线程,每次操作完(原子操作),调用Decrement方法是asyncOpts减少1。asyncOpts为0后,将通知主线程继续运行。如果将asyncOpts初始值写小点,就可以看到“操作结束”的输出会在线程输出之中。
几种互斥技术的比较:
1.Monitor.Enter和Monitor.Exit和lock(obj)都是基于对象锁技术.这里注意一点,如何去所定一个类(静态).如果尝试同时运用Monitor和lock,而且他们锁定的对象不一样时.结果会怎么样,这个我想大家都应该知道的.既然都是基于对象锁技术,锁是和临界资源捆绑在一起.用到这些的时候,一个要和一个对象关联的.这种方式粒度较粗。
2.Mutex是基于自身的锁.通过将一个临界资源更一个Mutex实力关联,要求所有请求该临界资源的线程首先获得更他相关的Mutex锁.这种方式的锁定力度可以自由控制,可以是一个对象,-段代码甚至整个进程。注意下,Mutex是从WaitHandle类派生而来,Mutex很多都是用WaitHandle的方法。
3.Interlocked提供粒度最细的锁,它不依赖于锁定,而基于原子操作的不可分割性,他使增,减,交换,比较等动作成为一个不可分割的原子操作实现互斥。
同步
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
.NET公共运行库实现同步的基本方法有三种。 前面很多地方已经用到同步的知识,这里我简单介绍下。 Thred.Join()这个方法阻塞调用线程,直到某个线程终止时为止。 Thread thread=new Thread(ThreadFun); thread.start(); thread.Join(); WaitHandle Waithandle类封装Win32同步句柄,并用于表示运行库中所有允许执行等待操作的同步对象。从WaitHandle派生的类包括Mutex,AutoResetEvent,ManualResetEvent。前面两个类已经被多次用到!这里不在介绍了。
ManualResetEvent是可以手动设置为非终止状态。手动重置的事件的状态始终保持终止,直到Reset方法将其设置为非终止状态。下面看下AutoResetEvent和ManualResetEvent的用法区别,
1 AutoResetEvent autoasyncOpIsDone = new AutoResetEvent (false); ThreadPool.QueueUserWorkItem( new WaitCallback(myFunction),autoasyncOpIsDone);
2 autoasyncOpIsDone.WaitOne(); //asyncOpIsDone自动被复位,这是AutoResetEvent 的用法
3
4 ManualResetEvent manualasyncOpIsDone =new ManualResetEvent(false); ThreadPool.QueueUserWorkItem( new WaitCallback(myFunction),manualasyncOpIsDone);
5 manualasyncOpIsDone.WaitOne(); manualasyncOpIsDone.Reset();//注意这句,手工复位。这是ManualResetEvent的用法。
Monitor和lock语句配合实现同步,这个给个例子:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading;
6
7 namespace ConsoleApplication13
8 {
9 class Program
10 {
11 static void Main(string[] args)
12 {
13 DemoBuffer();
14 }
15
16 public static void BufferRead()
17 {
18 Object o = AppDomain.CurrentDomain.GetData("Buffer");
19 if (o != null)
20 {
21 Buffer buffer = (Buffer)o;
22 for (int i = 0; i < 8; i++)
23 {
24 Console.WriteLine("读线程\t{0}读到字符{1}",
25 Thread.CurrentThread.GetHashCode(), buffer.Get());
26 }
27
28 }
29 Console.WriteLine("读取结束");
30 }
31
32 public static void BufferWrite()
33 {
34 object o = AppDomain.CurrentDomain.GetData("Buffer");
35 char[] msg = { 'E', 'N', 'U', 'O', 'e', 'n', 'u', 'o','t','v' };
36 if (o != null)
37 {
38 Buffer buffer = (Buffer)o;
39 for (int i = 0; i < msg.GetLength(0); i++)
40 {
41
42 Console.WriteLine("写线程\t{0}写字符{1}", Thread.CurrentThread.GetHashCode(), msg[i]);
43 buffer.Put(msg[i]);
44 }
45 }
46 Console.WriteLine("写入结束");
47 }
48
49 public static void DemoBuffer()
50 {
51 Buffer buffer = new Buffer();
52 AppDomain.CurrentDomain.SetData("Buffer", buffer);
53 Thread threadReader = new Thread(BufferRead);
54 Thread threadWrite = new Thread(BufferWrite);
55 threadReader.Start();
56 threadWrite.Start();
57 threadReader.Join();
58 threadWrite.Join();
59 Console.Read();
60 }
61
62 }
63 public class Buffer
64 {
65 const int size = 4;
66 char[] buffer = new char[size];
67 int n = 0, head = 0, tail = 0;
68 public void Put(char ch)
69 {
70 lock (this)
71 {
72 n++;
73 while (n > size)
74 {
75 // n = size;
76 Monitor.Wait(this);
77 }
78 buffer[tail] = ch;
79 tail = (tail + 1)%size;
80 //如果缓冲区为空,则通知所有等待线程
81 if (n <= 0)
82 {
83 Monitor.PulseAll(this);
84 Monitor.Wait(this);
85 }
86
87 }
88 }
89 public char Get()
90 {
91 char ch;
92 lock (this)
93 {
94 n--;
95 //如果缓冲区为空,则等待线程写入数据
96 while (n < 0)
97 {
98 // n = 0;
99 Monitor.PulseAll(this);
100 Monitor.Wait(this);
101 }
102 ch = buffer[head];
103 head = (head+ 1)%size;
104 //如果缓冲区满了,则通知所有的等待线程
105 if (n >= size)
106 {
107 Monitor.PulseAll(this);
108 }
109 Monitor.PulseAll(this);
110 return ch;
111 }
112 }
113 }
114
115 }
运行结果为
这个例子的运行顺序还是相当复杂的.首先执行BufferRead这个方法,此后执行Buffer的Get()方法,作完n--后,n=-1;注意一点,此时对象已被Get()方法锁定,Push()方法在等代锁,lock内的部分暂时还没执行.n=-1<0执行Monitor.PulseAll(this)和Monitor.Wait(this);通知等待线程锁定对象状态的更改.释放对象锁,直到再次获得锁.这是Push()方法运行,n=0,临界资源有一笔数据.n=0<=0又执行了上面两个方法.此时Get()将再次获得对象锁,读出数据,再次调用Monitor.PulseAll(this),最后lock里面代码执行结束,释放了锁,put()方法获得锁然后走出了lock里面的语句。至此n=0;然后BufferRead方法又执行,和上面的结果就一样了。(BufferRead基本上限于BufferWrite运行,可能会有不同,我试了好多次)。整个执行的过程有点繁琐。
下面对这三个做下总结:
1.Join用于等待特定的线程结束,经常用于主线程和子线程的同步。比如说,主线程需要等待某个线程做完某件事才继续,这时就可以用Join这个方法。
2.AutoResetEvent和ManualReset用事件信号的方式实现线程之间的同步。经常用的方法为WaitOne()和set();
3.Monitor和lock一般配合使用实现同步。