• 多线程


    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)
    {
        ...
    }
    

     

  • 相关阅读:
    第一道题:无头苍蝇装头术(望不吝赐教)
    jdk8 list是否包含某值的一些应用
    Failed to close server connection after message failures; nested exception is javax.mail.MessagingException: Can't send command to SMTP host
    itext pdf加密
    TiDB-禁用遥测功能
    TiDB-配置调整
    DM-表空间
    DM-INI参数配置
    DM-DSC集群配置
    PG-并行查询
  • 原文地址:https://www.cnblogs.com/houkui/p/4226623.html
Copyright © 2020-2023  润新知