线程同步手记
本文来源:
https://www.cnblogs.com/jackyfei/archive/2012/04/11/synchronization.html
一、前言
线程同步其实很简单,但是往往被老师教的很复杂。这是之前上课受的伤。脑袋瓜当人人家的跑马场,被蹂躏一番,最后老师留下的是先入为主的错误,以至于后面不停的干扰我的理解,纠起错来,真是不知道浪费了多少精力。
二、什么是线程同步
一直想要找一个良好的方式来表达什么是线程同步。
先看一个模拟线程同步的图:
假如这个盒子一次只能放一个东西,并且接力赛又要保持顺畅,该是怎样的情景?
首先对于Reader来说,取货的时候,箱子必须有货,如果没有货,要在旁边等候;
其次对于Writer来说,存货的时候,箱子必须为空,如果不为空,也要在旁边等候;
两个人要步调一致,并且配合默契,才能顺利的搬运东西。反过来,如果Reader执行了好几次,Writer才执行一次,或者Write执行了好几次,Reader才执行一次,最后都不能很好的保持步调的一致。
把两个人看成是两个线程,这时候线程要同步,也必须要满足上面的要求。两个线程要协同一致的工作,才能完成一项任务。
三、代码演示:
1 public class ThreadSyn
2 {
3 //缓存区,假设一次只能缓存一个字符
4 private static char buffer;
5 //线程1:写操作
6 public Thread thread1 = new Thread(()=>
7 {
8 string str = "横看成岭侧成峰,远近高低各不同。不识庐山真面目,只缘身在此山中。";
9 for (int i = 0; i < 32; i++)
10 {
11 buffer = str[i];
12 Thread.Sleep(26);
13 }
14
15 });
16
17 //线程2:读操作
18 public Thread thread2 = new Thread(() =>
19 {
20 for (int i = 0; i < 32; i++)
21 {
22 char ch = buffer;
23 Console.WriteLine(ch);
24 Thread.Sleep(36);
25 }
26 });
27 }
28
29 public class Program
30 {
31 static void Main(string[] args)
32 {
33 ThreadSyn threadSyn=new ThreadSyn();
34 threadSyn.thread1.Start();
35 threadSyn.thread2.Start();
36
37 Console.Read();
38 }
39 }
运行效果图:
四、原因和方案:
此时线程还是没有协同工作。因为如果写一个,读一个,再写一个,再读一个,那么这首诗应该是一首完整的显示。但是效果图的诗句却是紊乱的。
如何才能解决真正的同步,.net为我们提供了一系列的同步类。包括:互锁(Interlocked),管程(Monitor)和互斥体(Mutex).
4.1下面用互锁来解决上面的问题。
1 public class ThreadSyn
2 {
3 //缓存区
4 private static char _buffer;
5 //标示盒子,即缓冲区使用的空间,盒子初始化为0
6 private static long _box = 0;
7 //线程1:写操作
8 public Thread thread1 = new Thread(()=>
9 {
10 string str = "横看成岭侧成峰,远近高低各不同。不识庐山真面目,只缘身在此山中。";
11 for (int i = 0; i < 32; i++)
12 {
13 //写入之前检查缓冲区
14 //如果缓冲区已满,就进行等待,直到缓冲区的数据被进程读取为止
15 while(Interlocked.Read(ref _box) == 1)
16 {
17 Thread.Sleep(10);
18 }
19
20 //向缓冲区写数据
21 _buffer = str[i];
22 //写完数据,标记缓冲区已满
23 Interlocked.Increment(ref _box);
24 }
25 });
26
27 //线程2:读操作
28 public Thread thread2 = new Thread(() =>
29 {
30 for (int j = 0; j < 32; j++)
31 {
32 //写入之前检查缓冲区
33 //如果缓冲区为空,就进行等待,直到缓冲区的数据被进程填充为止
34 while(Interlocked.Read(ref _box) == 0)
35 {
36 Thread.Sleep(10);
37 }
38
39 //向缓冲区读数据
40 char ch = _buffer;
41 Console.Write(ch);
42 //读完数据,标记缓冲区已空
43 Interlocked.Decrement(ref _box);
44 }
45 });
46 }
运行效果图:
InterLocked提供了单个指令的操作,因此他提供了性能非常高的同步。
4.2用Monitor来解决问题
Monitor的原理是这样的:先执行的线程,独占锁,进入临界区,执行临界区资源代码。其他线程,只能在集中在临界资源上等待被叫唤。当独占锁推出资源区,也可以继续让自己等待,等待下一次被叫唤。
1 //缓存区
2 private static char _buffer;
3 //用于同步的对象
4 private static object _objForLock = new object();
5
6 //线程1:写操作
7 public Thread thread1 = new Thread(() =>
8 {
9 string str = "横看成岭侧成峰,远近高低各不同。不识庐山真面目,只缘身在此山中。";
10 for (int i = 0; i < 32; i++)
11 {
12 try
13 {
14 //进入临界区,获取独占锁
15 Monitor.Enter(_objForLock);
16
17 //向缓冲区写数据
18 _buffer = str[i];
19
20 //写完后,唤醒在临界资源上睡眠的线程
21 Monitor.Pulse(_objForLock);
22
23 //让当前线程睡眠在临界资源上
24 Monitor.Wait(_objForLock);
25
26 //整个流程有点像轮班吃饭,
27 //第一个人先去吃饭,第二个在值班等待,第一个吃完了,唤醒第二个吃饭,自己则在等待下一次吃饭。
28 }
29 catch (ThreadInterruptedException ex)
30 {
31 Console.WriteLine("线程被中断……");
32 }
33 finally
34 {
35 //退出临界区
36 Monitor.Exit(_objForLock);
37 }
38 }
39 });
40
41 //线程2:读操作
42 public Thread thread2 = new Thread(() =>
43 {
44 for (int j = 0; j < 32; j++)
45 {
46 try
47 {
48 //进入临界区,获取独占锁
49 Monitor.Enter(_objForLock);
50
51 //向缓冲区读数据
52 char ch = _buffer;
53 Console.Write(ch);
54
55 //写完后,唤醒在临界资源上睡眠的线程
56 Monitor.Pulse(_objForLock);
57
58 //让当前线程睡眠在临界资源上
59 Monitor.Wait(_objForLock);
60
61 }
62 catch (ThreadInterruptedException ex)
63 {
64 Console.WriteLine("线程被中断……");
65 }
66 finally
67 {
68 //退出临界区
69 Monitor.Exit(_objForLock);
70 }
71 }
72 });
不同的是Monitor只能锁定引用类型的对象,值类型会被装箱,等于生成另外一个对象,不能达到同步。为了保证推出临界区资源得到释放,使用了finally。为了方便使用,C#专门使用了lock语句。
所以我们可以完全更简洁的重写上面的try{}finally{}中的关键代码,如下所示:
1 lock (_objForLock)
2 {
3 //进入临界区,获取独占锁
4 Monitor.Enter(_objForLock);
5
6 //向缓冲区写数据
7 _buffer = str[i];
8
9 //写完后,唤醒在临界资源上睡眠的线程
10 Monitor.Pulse(_objForLock);
11
12 //让当前线程睡眠在临界资源上
13 Monitor.Wait(_objForLock);
14 }
独占锁注意:
因为独占锁,其他线程就不能再访问,只有Lock结束后,其他线程才可以访问,这保证了访问的正确性。但是,如果有多个线程对同一个资源进行写操作,在独占锁解开前,其他线程只能被临时暂停,这使得程序的效率大打折扣。所以应该慎用锁,只有必要时才使用。
● 微服务学习导航
● 微服务划分的姿势