《C#多线程编程实战》1.10 lock关键字
lock关键字是锁定资源用的。
书上的代码解释很好。
/// <summary> /// 抽象类 加减法 /// </summary> abstract class CounterBase { /// <summary> ///抽象 加法 方法 /// </summary> public abstract void Increment(); /// <summary> /// 抽象 减法 方法 /// </summary> public abstract void Decrement(); } /// <summary> /// 不使用lock关键字的实现抽象类Counter /// </summary> class Counter : CounterBase { public int Count { get; private set; } /// <summary> /// 非Lock关键字减法 /// </summary> public override void Decrement() { Count--; } /// <summary> /// 非lock关键字加法 /// </summary> public override void Increment() { Count++; } } /// <summary> /// 使用Lock关键字的类 /// </summary> class CounterWidthLock : CounterBase { /// <summary> /// 判断是否锁定资源 /// </summary> public readonly object _syncRoot = new object(); public int Count { get; private set; } /// <summary> /// Lock关键字减法 /// </summary> public override void Decrement() { lock (_syncRoot)//lock关键字,锁定资源 { Count--; } } /// <summary> /// lock关键字加法 /// </summary> public override void Increment() { lock (_syncRoot)//lock关键字,锁定资源 { Count++; } } } /// <summary> /// 测试主类 /// </summary> class Program { /// <summary> /// 测试主程序入口 /// </summary> /// <param name="args"></param> static void Main(string[] args) { Console.WriteLine("不使用Lock关键字 Counter类"); var NotLockClass = new Counter(); var t1 = new Thread(() =>TestCounter(NotLockClass)); var t2 = new Thread(() => TestCounter(NotLockClass)); var t3= new Thread(() => TestCounter(NotLockClass)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); Console.WriteLine($"最终输出结果是{NotLockClass.Count}"); Console.ReadKey(); } static void TestCounter(CounterBase counterBase) { for(int i=0;i<10000;i++) { counterBase.Increment();//加法 counterBase.Decrement();//减法 } } }
上面这一部分是,未使用lock关键字的代码。
结果也如同书上说count是未定。多次启动程序 结果也是不一样的。正确的结果应该是0,加一次见一次正好分别是5K次。
仔细观察代码,得到这样子的结果也是肯定的。
首先:
var NotLockClass = new Counter(); var t1 = new Thread(() =>TestCounter(NotLockClass)); var t2 = new Thread(() => TestCounter(NotLockClass)); var t3= new Thread(() => TestCounter(NotLockClass));
这个部分,是三个实例的Thread共享了一个Counter的实例化对象。
那么,要肯定的是这个Counter是一个引用类型
那么引用类型发挥在哪里呢?
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
这里。
首先是T1 T2 T3三个线程启动。
然后是等待线程完成。
让结果变得不固定的原因就在这里。
我们先把方法TestCounter改造一下
static void TestCounter(CounterBase counterBase) { for(int i=0;i<10000;i++) { counterBase.Increment();//加法 Console.WriteLine($"加法启动次序{i}"); counterBase.Decrement();//减法 Console.WriteLine($"减法启动次序{i}"); } }
加入一下两个控制台输出语句。
然后启动程序。
会发现有很多重复的顺序。而这个就是资源抢夺。
为了更清楚的看执行的过程,我们再来改造一下。
static void TestCounter(Counter counter) { for(int i=0;i<10000;i++) { counter.Increment();//加法 Console.WriteLine($"加法启动次序{i},当前结果是{counter.Count}"); counter.Decrement();//减法 Console.WriteLine($"减法启动次序{i},当前结果是{counter.Count}"); } }
之后在启动
资源抢夺还是很严重的。 不过你们有没有发现。方法改造之后的结果和正确结果很相近了。不是-1,就是-2.这是一个很有意思的现象。
如果你将代码改造成:
class Counter : CounterBase { public int Count { get; private set; } /// <summary> /// 非Lock关键字减法 /// </summary> public override void Decrement() { Console.WriteLine($"减法 当前{Count}"); Count--; } /// <summary> /// 非lock关键字加法 /// </summary> public override void Increment() { Console.WriteLine($"加法 当前{Count}"); Count++; } }
结果:
竟然会出现正确答案,而且几率很高。得到-1这个答案我运行了很多次。-2更是看运气。
为什么会出现这个现象。我觉得是可以程序的方法可运行时间有一定关系吧。时间可能是多了一下。资源调度上面可能会分配。也可能是线程的原因。这个问题真的很有意思。
不过还是先放放。
我们依旧是得到未使用lock关键字,资源抢夺很严重!
那么我们来看看是使用lock关键字的部分。
改造代码:
static void Main(string[] args) { Console.WriteLine("使用Lock关键字 CounterWidthLock类"); var UseLockClass = new CounterWidthLock(); var t1 = new Thread(() =>TestCounter(UseLockClass)); var t2 = new Thread(() => TestCounter(UseLockClass)); var t3= new Thread(() => TestCounter(UseLockClass)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); Console.WriteLine($"最终输出结果是{UseLockClass.Count}"); Console.ReadKey(); }
肯定是正确答案。
我们改造一下代码:和上一样:
static void TestCounter(CounterBase counterBase) { for(int i=0;i<10000;i++) { counterBase.Increment();//加法 Console.WriteLine($"加法启动次序{i}"); counterBase.Decrement();//减法 Console.WriteLine($"减法启动次序{i}"); } }
看一下是否会出现资源抢夺
其实还是会出现的。
但是为什么结果会是正确的呢?
我们再来改造一下:
static void TestCounter(CounterWidthLock counterwidthlock) { for(int i=0;i<10000;i++) { counterwidthlock.Increment();//加法 Console.WriteLine($"加法启动次序{i},当前的结果是{counterwidthlock.Count}"); counterwidthlock.Decrement();//减法 Console.WriteLine($"减法启动次序{i},当前的结果是{counterwidthlock.Count}"); } }
很明显,执行的顺序还是混乱。但是为什么结果却是正确呢?
我们来正式的讲讲lock关键字了
第一步 是lock建立互斥锁。
第二步执行lock内的方法执行完毕之后,
第三步释放lock互斥锁。
这一个过程中,只有一个线程能访问,如果有其他线程访问那就必须等待第一个线程执行完,并释放lock。
那么在代码中
1 class CounterWidthLock : CounterBase 2 { 3 /// <summary> 4 /// 判断是否锁定资源 5 /// </summary> 6 public readonly object _syncRoot = new object(); 7 8 public int Count { get; private set; } 9 10 /// <summary> 11 /// Lock关键字减法 12 /// </summary> 13 public override void Decrement() 14 { 15 lock (_syncRoot)//lock关键字,锁定资源 16 { 17 Count--; 18 } 19 } 20 /// <summary> 21 /// lock关键字加法 22 /// </summary> 23 public override void Increment() 24 { 25 lock (_syncRoot)//lock关键字,锁定资源 26 { 27 Count++; 28 } 29 } 30 }
6:锁
15,25均为lock关键字。
那么什么是锁?
可以理解为一个唯一的资源,对象。这个对象最好不是公开的。 公开的话 会造成很多不便。
也就是T1 T2 T3 虽然都同时进行了TestCounter的方法。
但因为lock的存在,一个线程在执行加减的时候,其他线程是不可以干预的。也就是T1 执行加减,也许执行了三四次,T2 T3一直在等待。三个线程都是彬彬有礼等待其他来完成他们自己的事情,之后在是自己的。虽然谦让,但也有自己的原则,就是自己在做的时候,别人是不可以干预的