前言:上一期讲了互斥和同步的基本概念,而且用金矿和苦工的例子讲了信号量。
我们继续用金矿和苦工的例子举例,但是这一次我们不再是用控制台来简单的输出是否正在挖矿了,而是我们开一个变量用来事实的去模拟金钱的增加。
我们就让5个矿工每个矿工挖矿100次
using System;
using System.Threading;
namespace LeeCarry
{
public class Test
{
private static int gold=0;
public static void Main(string[] args)
{
for(int i=1;i<=5;i++)
{
Thread t = new Thread(DoMining);
t.Start(i);
}
}
private static void DoMining(object obj)
{
int id=Convert.ToInt32(obj);
for(int i=0;i<100;i++)
{
gold++;
}
Console.WriteLine("我是{0}号挖矿完成,现在一共有{1}黄金",id,gold);
}
}
}
输出结果:
为什么会是这个样子呢,这实际上就是上一期提到的互斥问题,因为他们都想去读写同一个变量,例如A和B都想执行一次x=x+1,x假如当前是3的话,A读取到了x是3,然后可以解析为x=4。B如果是并行和A执行到x=x+1的话(此时A还没写进去),也是解析为x=4,那么最终x的结果就为4了,但我们都知道如果执行两次x=x+1应该是5才对。
那怎么解决这个问题呢?在C#当中很方便,直接用一个lock就可以了,lock参数锁住的对象必须得是一个引用类型,推荐使用object,具体的分析我们先等看了代码后再说。之后在lock语句块里面放我们要执行的内容即可。
using System;
using System.Threading;
namespace LeeCarry
{
public class Test
{
private static readonly object flag=new object();
private static int gold=0;
public static void Main(string[] args)
{
for(int i=1;i<=5;i++)
{
Thread t = new Thread(DoMining);
t.Start(i);
}
}
private static void DoMining(object obj)
{
int id=Convert.ToInt32(obj);
for(int i=0;i<100;i++)
{
lock(flag)
{
gold++;
}
}
Console.WriteLine("我是{0}号挖矿完成,现在一共有{1}黄金",id,gold);
}
}
}
结果:
当然这里因为几乎是同时完成的所以控制台输出顺序不一定,但是至少我们的lock语句块里面要执行的东西是不会冲突了。
使用lock锁对象的时候还要注意以下几点,原则是要锁的这个对象最好除了lock以外没有任何地方使用到
所以还是更推荐用private readonly object(这里的readonly一个引用也是一个考点,包括readonly和cosnt区别等)。
要知道lock只是C#的一个语法糖(syntactic sugar),糖虽然好吃,但是不注意的话会蛀牙的噢。
让我们先来第一步分解lock,假设x是一个引用类型,那么
lock(x)
{
//要执行的代码
}
可以看作是(前提是使用System.Threading命名空间)
Monitor.Enter(flag);
//要执行的代码
Monitor.Exit(flag);
当然这样子是不严谨的,为什么?因为即使lock语句的正文中引发异常,也会释放 lock,所以要改成
try
{
Monitor.Enter(flag);
//要执行的代码
}
finally
{
Monitor.Exit(flag);
}
当然可以写的更严谨一点,即完全等同于(来自MSDN官方解释)
object __lockObj = x;
bool __lockWasTaken = false;
try
{
System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);
// Your code...
}
finally
{
if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}
这里就可以看到我们的lock其实就是Monitor的一个使用方式,那么Monitor又是一个什么呢?Monitor其实是一个混合锁,混合了什么呢?我很想在这个地方就直接展开来说,但是这样这一章篇幅就太长了,留到下一章再展开来看吧,本章还是着重看一下lock和Monitor
首先昨天讲了四种解决互斥与同步问题的方法,如果要把lock套上其中一种方法的话那就是临界区。
这里再重新介绍一下临界区(critical section),就是标识一段代码为临界区,每次只准许一个进程进入临界区,进入后不允许其他进程进入。
的确它的设计思路就是来源于临界区的,又或者说临界区本身就是一种设计思路,MSDN官方上也有说可以使用lock标识临界区,这当然是完全正确的,但C#中lock封装的Monitor能做的比临界区要多。
现在我们即使看到了lock剥去一层外衣后知道是使用Monitor后应该还是会有一个疑问,为什么锁住的是一个引用类型就可以标识他们能同步?这里就要先讲讲同步块和同步块索引了
同步块是.NET中解决对象同步问题的基本机制,该机制为每个堆内的对象(即引用类型对象实例)分配一个同步索引,该索引中只保存一个表明数组内索引的整数。具体过程是:.NET在加载时就会新建一个同步块数组,当某个对象需要被同步时,.NET会为其分配一个同步块,并且把该同步块在同步块数组中的索引加入该对象的同步块索引中。
同步块机制包含以下几点:
引用:
lock 语句-MSDN
lock 语句2-MSDN
C++ 临界区 多线程同步互斥-lzg13541043726
C#中lock死锁实例教程-刘奇云
揭示同步块索引(上):从lock开始-横刀天笑的碎碎念
.NET基础拾遗(5)多线程开发基础-Edison Zhou
.NET面试题解析(07)-多线程编程与线程同步-/梦里花落知多少/