多线程 - 线程同步锁(lock、Monitor)

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)
{
    ...
}
时间: 2024-07-31 06:00:20

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

C# 同步锁 lock Monitor

Lock关键字 C#提供lock关键字实现临界区,MSDN里给出的用法: Object thisLock = new Object();lock (thisLock){   // Critical code section} 还有一种是比lock更“高级”的Monitor: private static object obj = new object(); public static void SomeMethod() { bool lockTaken=false; //假定没有获取锁 try

多线程&线程同步

线程 程序执行过程中,并发执行的代码段. 线程之间可以共享内存. 线程安全 增加了同步处理,确保在同一时刻,只有一个线程执行同步代码. 保证线程安全的方法就是锁机制 java中的任何对象都可以作为锁对象 synchronized(lock){....} 代码块中的代码被确保同一时间只有一个线程才能执行 同步方法是用当前对象作为同步对象(this) public synchronized int getTicket(){...} synchronized关键字也能加在方法上 确保同一时间只有一个线

线程同步锁、死锁、递归锁、信号量、GIL

目录 线程同步锁.死锁.递归锁.信号量.GIL 一.同步锁 二.死锁 三.递归锁(Rlock) 四.信号量(Semphare) 五.GIL(全局解释器锁) io密集型 计算密集型 线程同步锁.死锁.递归锁.信号量.GIL 一.同步锁 所有线程同一时间读写同一个数据,有的线程已经对数据进行修改了,造成有的线程拿到的数据时旧的数据,而不是修改后的数据,造成结果不正确,于是引入了同步锁解决问题, 同步锁的原理是同一时间只能有一个线程读写数据. 锁通常被用来实现对共享资源的同步访问.从threading

python线程互斥锁Lock(29)

在前一篇文章 python线程创建和传参 中我们介绍了关于python线程的一些简单函数使用和线程的参数传递,使用多线程可以同时执行多个任务,提高开发效率,但是在实际开发中往往我们会碰到线程同步问题,假如有这样一个场景:对全局变量累加1000000次,为了提高效率,我们可以使用多线程完成,示例代码如下: # !usr/bin/env python # -*- coding:utf-8 _*- """ @Author:何以解忧 @Blog(个人博客地址): shuopython

swift详解之十五------------NSThread线程同步锁

NSThread线程同步锁 上小节用NSThread实现读取网络图片 , 这节用NSThread 模拟一个卖票的例子 .用NSLock 或者NSCondition 锁定资源 var total = 100 //总票数 var w1 = 0 //窗口1卖出票数 var w2 = 0 //窗口2卖出票数 var isSell = true //是否出售 var lock:NSLock? var condition:NSCondition? 这里首先定义一些变量 然后再点击卖票的时候,开启连个线程模仿

C# 线程同步之排它锁/Monitor监视器类

一.Monitor类说明,提供同步访问对象的机制. 1.位于System.Threading命名空间下,mscorlib.dll程序集中. 2.Monitor通过获取和释放排它锁的方式实现多线程的同步问题. 3.Monitor实现当前进程内的多线程的同步,和lock语句的功能类似. 4.当前为静态类,使用简单 5.依赖的锁定对象和lock语句中类似,同样需要引用类型.建议私有.只读.静态 定义代码: // // 摘要: // 提供同步访问对象的机制. [ComVisible(true)] pub

【多线程-线程同步】

线程同步:协调多个线程间的并发操作,以获得符合预期的,确定的执行结果,消除多线程应用程序的不确定性. 使用线程的同步:可以保护资源同时只能由一个线程访问,一般采取的措施是获取锁,释放锁.即锁机制:可以协调线程的访问顺序,即某一资源只能先由线程A访问,再由线程B进行访问. class Program { private static Thread subthread ; private static int i; static void Main(string[] args) { subthrea

Java多线程 线程同步

如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你需要使用同步,并且,读写线程都必须用相同的监视器锁同步.--Brain同步规则 synchronized 所有对象都自动含有单一的锁,当在调用一个对象的任意synchronized方法时,此对象将被加锁. 对于某个特定对象来说,所有的synchronized方法共享同一个锁.所以某个线程在访问对象的一个synchronized方法时,其他线程访问该对象的任何synchronized方法都

9 C++ Boost 多线程,线程同步

线程的创建 boost_thread,boost_system 多线程的创建 线程的参数传递 线程的创建方式 线程的join 加入join,回收线程 线程中断 线程中断2, 线程组 boost 线程的死锁 boost 线程递归锁 线程互斥锁,线程同步 unique_lock 锁,离开作用域自动释放 unique_lock 锁 示例 2,可以显式的释放锁 boost 1次初始化 boost 条件变量 boost 线程锁,一个账户往另外一个账户转钱案例 boost upgrade_lock 知识背景