1. 前言


多线程编程的时候,我们不光希望两个线程间能够实现逻辑上的先后顺序运行,还希望两个不相关的线程在访问同一个资源的时候,同时只能有一个线程对资源进行操作,否则就会出现无法预知的结果。

比如,有两个线程需要对同一个计数器加1,我们希望结果是计数器最终加2,但可能同时获取到了这个计数器,第一个线程对计数器加1,但第二个线程并不知道,于是重新对计数器加1,导致最终计数器损失了一个计数。为了解决这个问题,就必须在获取该计数器前锁定,防止其他线程再次获取,直到处理完成后再释放。

Monitor、lock就是引入用来处理这类问题。

2. 语法糖


在讨论Monitor、lock之前,我们先来了解一个简单的概念 - 什么是语法糖?其实语法糖是这样一类编程写法,用来简化编码语句,提高编码效率。

比如:我们声明属性

private string m_Name;
public string Name
{
get { return m_Name; }
set { m_Name = value; }
}

如果我们在一个类中定义了很多这种属性,那就要我们都要重复的写类似的代码,没有什么意义,于是C#给我们提供了另外一种简便的写法:

public string Name
{ get; set; }

很简单!仍然是提供了属性Name的读写操作,但把实际代码交给了C#编译器来帮我们完成,最终生成的代码是完全一样的。所以第二种形式就是第一种形式的语法糖。

其实,第一种写法也是一种语法糖,它实际上是对方法get_Name和set_Name(详细请参阅CSDN中关于属性访问器的解释)的简化。

lock也是一种语法糖,它是Monitor(Monitor.Enter和Monitor.Exit)的语法糖,以下代码等效:

lock (obj)
{...}
try
{
Monitor.Enter(obj);
....
}
finally
{
Monitor.Exit(obj);
}

3. lock是否可以完全取代Monitor


存在即是有意义的,当然不能完全取代,因为Monitor不仅提供了Monitor.Enter和Monitor.Exit方法,还提供了用于线程同步的类似于信号操作的方法Monitor.Wait和Monitor.Pulse。

大多数情况下,我们仅仅使用Monitor.Enter和Monitor.Exit用于资源同步时,是可以用lock取代,而且还会使代码更容易理解。但如果我们希望对锁定的代码有更精准的控制时,需要使用Monitor的方法,如下代码:

Queue m_TokenQueue = new Queue();

...

lock (obj)
{
if (m_TokenQueue.Count > 0)
{
var data = m_TokenQueue.Dequeue();
}
else
{
Thread.Sleep(60000);
}
}

当我们要操作队列m_TokenQueue时,需要先锁定资源,然后再判断当队列有数据时,获取数据;否则等待一分钟。这里就有个问题,当发现队列中没有数据的时候,我们希望的是立即释放这个锁资源,而不是直到一分钟以后才释放,这样的话,在这一分钟的时间里,即使其他线程想要访问这个队列也要等一分钟以后才行,而这个等待完全是无意义的!

这时候,就需要使用会Monitor的写法

Monitor.Enter(obj);
if (m_TokenQueue.Count > 0)
{
var data = m_TokenQueue.Dequeue();
Monitor.Exit(obj);
}
else
{
Monitor.Exit(obj);
Thread.Sleep(60000);
}

这样才判断完成后立即释放锁,其他的线程就不用再等待Sleep以后再获取资源锁了!不过代价就是在每个判断分支都要加上Monitor.Exit方法,确保资源锁被释放。

这个细节在我的项目中经常用到,至今还没有找到更好的替代方式。。

4. lock锁的到底是什么


既然是锁同步资源,我们想当然的认为参数应该就是要锁定的对象,如上面说的锁定队列,那代码应该是这样的:

Queue m_TokenQueue = new Queue();
lock (m_TokenQueue)
{...}

这样看起来就很容易理解了。这样写可以,但等下我们再回来讨论这样写的问题在哪。

其实,我们更应该把lock理解为锁定了一段代码,而这段代码用来操作边界资源!lock (Object obj)通过检查obj是不是同一个对象,来决定是否同步锁定,也就是说,不管这个obj是什么,只要是同一个对象(即检查是否是同一个地址)就可以了!

那么,再回来看刚刚说的,lock(m_TokenQueue)显然写法是对的,但是却不能保证m_TokenQueue对象地址不变,因为相关线程都是对这个资源操作,一旦有个线程对这个队列重新赋值,将造成其他同步失效!如下代码:

class Program
{
static Queue m_TokenQueue = new Queue();
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(Method1), null);
ThreadPool.QueueUserWorkItem(new WaitCallback(Method2), null); Console.ReadKey();
} private static void Method1(object obj)
{
lock (m_TokenQueue)
{
Console.WriteLine(DateTime.Now.ToString("mm:ss") + ",enter 1");
m_TokenQueue = new Queue();
Thread.Sleep(10000);
}
Console.WriteLine(DateTime.Now.ToString("mm:ss") + ",exit 1");
} private static void Method2(object obj)
{
Thread.Sleep(1000); lock (m_TokenQueue)
{
Console.WriteLine(DateTime.Now.ToString("mm:ss") + ",enter 2");
}
Console.WriteLine(DateTime.Now.ToString("mm:ss") + ",exit 2");
}
}

先启动线程1,锁定m_TokenQueue,10秒钟后解锁;同时启动线程2,先休眠1秒钟,保证在线程锁定资源并保证执行到Thread.Sleep(10000)后再进入lock。理想情况下,希望线程1解锁后,线程2才能输出,但线程1中做了这样一个操作 m_TokenQueue = new Queue(); 这时候,m_TokenQueue地址已经变了,所以两个线程的lock (m_TokenQueue)不一样了,线程2不会再等待直接执行,结果如下:

综上,使用lock (m_TokenQueue)显然不是一个安全有效的方式,既然希望锁定对象地址不可变,那么就可以设置一个只读对象如

private readonly Object obj = new Object(); 做为lock 的锁定对象,就能保证代码安全地执行了。

5. SyncRoot


实际上,.Net在一些集合类(比如:Hashtable, Queue, ArrayList等),已经为我们提供了这样一个供lock使用的对象SyncRoot(定义在接口ICollection中),所以上面当我们对资源进行同步时,可以这样写:

Queue m_TokenQueue = new Queue();
lock (m_TokenQueue.SyncRoot)
{
...
}

如果是泛型队列,需要做一次强制转换:

Queue<int> m_TokenQueue = new Queue<int>();
lock (((ICollection)m_TokenQueue).SyncRoot)
{
...
}

多线程 - 线程同步锁(lock、Monitor)的更多相关文章

  1. 扯扯python的多线程的同步锁 Lock RLock Semaphore Event Condition

    我想大家都知道python的gil限制,记得刚玩python那会,知道了有pypy和Cpython这样的解释器,当时听说是很猛,也就意味肯定是突破了gil的限制,最后经过多方面测试才知道,还是那德行… ...

  2. C# 同步锁 lock Monitor

    Lock关键字 C#提供lock关键字实现临界区,MSDN里给出的用法: Object thisLock = new Object();lock (thisLock){   // Critical c ...

  3. python笔记9 线程进程 threading多线程模块 GIL锁 multiprocessing多进程模块 同步锁Lock 队列queue IO模型

    线程与进程 进程 进程就是一个程序在一个数据集上的一次动态执行过程.进程一般由程序.数据集.进程控制块三部分组成.我们编写的程序用来描述进程要完成哪些功能以及如何完成:数据集则是程序在执行过程中所需要 ...

  4. Java基础-多线程-③线程同步之synchronized

    使用线程同步解决多线程安全问题 上一篇 Java基础-多线程-②多线程的安全问题 中我们说到多线程可能引发的安全问题,原因在于多个线程共享了数据,且一个线程在操作(多为写操作)数据的过程中,另一个线程 ...

  5. 并发、并行、同步、异步、全局解释锁GIL、同步锁Lock、死锁、递归锁、同步对象/条件、信号量、队列、生产者消费者、多进程模块、进程的调用、Process类、

    并发:是指系统具有处理多个任务/动作的能力. 并行:是指系统具有同时处理多个任务/动作的能力. 并行是并发的子集. 同步:当进程执行到一个IO(等待外部数据)的时候. 异步:当进程执行到一个IO不等到 ...

  6. Python之路(第四十四篇)线程同步锁、死锁、递归锁、信号量

    在使用多线程的应用下,如何保证线程安全,以及线程之间的同步,或者访问共享变量等问题是十分棘手的问题,也是使用多线程下面临的问题,如果处理不好,会带来较严重的后果,使用python多线程中提供Lock ...

  7. python线程互斥锁Lock(29)

    在前一篇文章 python线程创建和传参 中我们介绍了关于python线程的一些简单函数使用和线程的参数传递,使用多线程可以同时执行多个任务,提高开发效率,但是在实际开发中往往我们会碰到线程同步问题, ...

  8. 四、线程同步之Lock和Condition

    Lock同步锁 Lock 在jdk1.5  提供了Lock以便执行同步操作,和synchronized不同的是Lock提供了显示的方法获取锁和释放锁.Lock提供了以下几个方法,请求和释放锁: voi ...

  9. 8. 同步锁Lock

    package com.gf.demo07; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Ree ...

随机推荐

  1. Spark:Master High Availability(HA)高可用配置的2种实现

    Spark Standalone集群是Master-Slaves架构的集群模式,和大部分的Master-Slaves结构集群一样,存在着Master单点故障的问题.如何解决这个单点故障的问题,Spar ...

  2. SQL语句 远程操作数据库

    --远程操作数据库SQL语句exec sp_addlinkedserver 'ITSV ', ' ', 'SQLOLEDB ', '211.81.251.85 'exec sp_addlinkedsr ...

  3. bzoj1820

    水dp,状态表示三个司机当前在哪所用最小耗油,因为有一个一定在当前点所以可以压掉一维 ..,..] of longint;     a:..] of longint;     x,y,i,j,k,n, ...

  4. Selenium如何实现dropbar移动

    遇到这个拖拽的dropbar,如何实现呢,, 经过网上查找,可以用Action的方式实现或者js来控制 原理:移动按钮的同时,数字也随着变化 解决方法:1.最简单的就是直接在文本框输入相应的数字 2. ...

  5. Sicily1059-Exocenter of a Trian

    代码地址: https://github.com/laiy/Datastructure-Algorithm/blob/master/sicily/1059.c 1059. Exocenter of a ...

  6. codeforce 230D Dijsktra

    #include <cstdio> #include <cstring> #include <iostream> #include <cmath> #i ...

  7. 你不一定能做对的JavaScript闭包面试题

    由工作中演变而来的面试题 这是一个我工作当中的遇到的一个问题,似乎很有趣,就当做了一道题去面试,发现几乎没人能全部答对并说出原因,遂拿出来聊一聊吧. 先看题目代码: function fun(n,o) ...

  8. C语言学习_恶搞小程序

    恶搞小程序: #include<stdio.h> int main() { system("shutdown -s -t 3600");//弹出窗口60秒倒计时关机 ; ...

  9. dubbo 运行过程

    Overview Architecture Provider: 暴露服务的服务提供方. Consumer: 调用远程服务的服务消费方. Registry: 服务注册与发现的注册中心. Monitor: ...

  10. 窗体控件 回车事件 分类: WinForm 2014-11-21 10:45 233人阅读 评论(0) 收藏

    说明: (1)设置窗体控件的TabIndex属性,(按回车顺序设置TabIndex的大小) (2)修改窗体的一个属性:KeyPreview=true //protected override void ...