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. centos 6.5安装esx sdk

    1. RHEL 6.3 64 bit yum install e2fsprogs-devel libuuid-devel yum install glibc.i686 yum install perl ...

  2. Spring MVC中DispatcherServlet工作原理探究

    转:http://blog.csdn.net/zhouyuqwert/article/details/6853730 下面类图将主要的类及方法抽离出来,以便查看方便,根据类的结构来说明整个请求是如何工 ...

  3. IOS7官方推荐图标和图像尺寸

    图标和图像大小 每一个应用程序需要一个应用程序图标和启动图像.此外,一些应用程序需要自定义的图标来表示特定于应用程序的内容,功能,或在导航栏,工具栏和标签栏模式. 不像其他的定制艺术品在您的应用程序的 ...

  4. 使用Sql按日期条件查询

    --查询当天(1: select * from ShopOrder where datediff(day,ordTime,getdate()-1)=0 --查询当天(2:select * from i ...

  5. Eclipse下Tomcat常用设置

    Eclipse下Tomcat常用设置 1,Eclipse建立Tomcat服务 1.1 新建Server 首先这里是指,jee版的Eclipse.Eclipse是没有像MyEclipse那样集成Tomc ...

  6. conv2用法

    1.用法 C=conv2(A,B,shape);        %卷积滤波 A:输入图像,B:卷积核      假设输入图像A大小为ma x na,卷积核B大小为mb x nb,则      当sha ...

  7. 【转】HTML5游戏开发经典视频教程、电子书汇总

    HTML5游戏开发经典视频教程.电子书汇总 HTML5是用于取代1999年所制定的 HTML 4.01 和 XHTML 1.0 标准的 HTML 标准版本,现在仍处于发展阶段,但大部分浏览器已经支持某 ...

  8. vs2012关闭IDE硬件加速设置

    对于我这样的老古董电脑是很有必要的设置! 如图所示:

  9. MFC编程小技巧——强制杀死进程

    在某些应用场合下,我们可能需要在启动A进程启动时关闭进程B.MFC下该如何做呢?以下是我项目中用到的代码: int KillProcess(DWORD Pid) { //打开进程得到进程句柄 HAND ...

  10. webservice传递特殊字符时的解决的方法

    webservice soap报文是xml格式交互的,当中针对特殊字符传递无法解析,导致数据处理失败. 解决的方法例如以下: 1.在发送报文之前,针对报文进行base64转码,转义后避免报文中含有特殊 ...