并发编程 - 线程同步(七)之互斥锁Monitor

通过前面对锁lock的基本使用以及注意事项的学习,相信大家对锁的同步机制有了大致了解,今天我们将继续学习------互斥锁Monitor。

lock是C#语言中的关键字,是语法糖,lock语句最终会由C#编译器解析成Monitor类实现相关语句。

例如以下lock语句:

csharp 复制代码
lock (obj)
{
    //同步代码块
}

最终会被解析成以下代码:

csharp 复制代码
Monitor.Enter(obj);
try
{
    //同步代码块
}
finally
{
    Monitor.Exit(obj);
}

lock关键字简洁且易于使用,而Monitor类 则功能强大,能够提供比lock关键字更细粒度、更灵活的控制以及更多的功能。

因为lock关键字是Monitor类的语法糖,因此lock关键字面临的问题,Monitor类同样也会面临。当然也会存在一些Monitor类特有的问题。

下面我们一起详细学习Monitor类的注意事项以及实现一个简单的生产者-消费者模式示例代码。

01、避免锁定值类型

这是因为 Monitor.Enter方法的参数为Object类型,这就导致如果传递值类型会导致值类型被装箱,进而导致线程在已装箱的对象上获取锁,最终线程每次调用Monitor.Enter方法都在一个完全不同的对象上获取锁,导致锁失效,无法实现线程同步。

看看下面这个代码示例:

csharp 复制代码
public class LockValueTypeExample
{
    private static readonly int _lock = 88;
    public void Method1()
    {
        try
        {
            Monitor.Enter(_lock);
            var threadId = Thread.CurrentThread.ManagedThreadId;
            Console.WriteLine($"线程 {threadId} 通过 lock(值类型) 锁进入 Method1");
            Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");
            Console.WriteLine($"开始休眠 5 秒");
            Console.WriteLine($"------------------------------------");
            Thread.Sleep(5000);
        }
        finally
        {
            Console.WriteLine($"开始释放锁 {DateTime.Now:HH:mm:ss}");
            Monitor.Exit(_lock);
            Console.WriteLine($"完成锁释放 {DateTime.Now:HH:mm:ss}");
        }
    }
}
public static void LockValueTypeRun()
{
    var example = new LockValueTypeExample();
    var thread1 = new Thread(example.Method1);
    thread1.Start();
}

看看执行结果:

可以发现在释放锁的时候抛出异常,大致意思是:"对象同步方法在未同步的代码块中被调用。",这就是因为锁定的地方和释放的地方锁已经不一样了。

02、小心try/finally

如上面的例子,Monitor.Enter方法是写在try块中,试想一下:如果在Monitor.Enter方法之前抛出了异常会怎样异常?看下面这段代码:

csharp 复制代码
public class LockBeforeExceptionExample
{
    private static readonly object _lock = new object();
    public void Method1()
    {
        try
        {
            if (new Random().Next(2) == 1)
            {
                Console.WriteLine($"在调用Monitor.Enter前发生异常");
                throw new Exception("在调用Monitor.Enter前发生异常");
            }
            Monitor.Enter(_lock);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"捕捉到异常:{ex.Message}");
        }
        finally
        {
            Console.WriteLine($"开始释放锁 {DateTime.Now:HH:mm:ss}");
            Monitor.Exit(_lock);
            Console.WriteLine($"完成锁释放 {DateTime.Now:HH:mm:ss}");
        }
    }
}
public static void LockBeforeExceptionRun()
{
    var example = new LockBeforeExceptionExample();
    var thread1 = new Thread(example.Method1);
    thread1.Start();
}

上面代码是在调用Monitor.Enter方法前随机抛出异常,当发生异常后,可以在释放锁的时候和锁定值类型报了同样的错误,执行结果如下:

这是因为还没有执行锁定就抛出异常,导致释放一个没有锁定的锁。

那要如何解决这个问题呢?Monitor类已经考虑到了这种情况,并给出了解决办法------使用Monitor.Enter的第二个参数lockTaken,当获取锁定成功则更改lockTaken为true。如此在finally的时候只需要判断lockTaken即可决定是否需要执行释放锁操作,具体代码如下:

csharp 复制代码
public class LockSolveBeforeExceptionExample
{
    private static readonly object _lock = new object();
    public void Method1()
    {
        var lockTaken = false;
        try
        {
            if (new Random().Next(2) == 1)
            {
                Console.WriteLine($"在调用Monitor.Enter前发生异常");
                throw new Exception("在调用Monitor.Enter前发生异常");
            }
            Monitor.Enter(_lock,ref lockTaken);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"捕捉到异常:{ex.Message}");
        }
        finally
        {
            if (lockTaken)
            {
                Console.WriteLine($"开始释放锁 {DateTime.Now:HH:mm:ss}");
                Monitor.Exit(_lock);
                Console.WriteLine($"完成锁释放 {DateTime.Now:HH:mm:ss}");
            }
            else
            {
                Console.WriteLine($"未执行锁定,无需释放锁");
            }
        }
    }
}
public static void LockSolveBeforeExceptionRun()
{
    var example = new LockSolveBeforeExceptionExample();
    var thread1 = new Thread(example.Method1);
    thread1.Start();
}

执行结果如下:

03、善用TryEnter

我们知道使用锁应当避免长时间持有锁,长时间持有锁会阻塞其他线程,影响性能。我们可以通过Monitor.TryEnter指定超时时间,可以看看下面示例代码:

csharp 复制代码
public class LockTryEnterExample
{
    private static readonly object _lock = new object();
    public void Method1()
    {
        try
        {
            Monitor.Enter(_lock);
            Console.WriteLine($"Method1 | 获取锁成功,并锁定 5 秒");
            Thread.Sleep(5000);
        }
        finally
        {
            Monitor.Exit(_lock);
        }
    }
    public void Method2()
    {
        Console.WriteLine($"Method2 | 尝试获取锁");
        if (Monitor.TryEnter(_lock, 3000))
        {
            try
            {
            }
            finally
            {
            }
        }
        else
        {
            Console.WriteLine($"Method2 | 3 秒内未获取到锁,自动退出锁");
        }
    }
    public void Method3()
    {
        Console.WriteLine($"Method3 | 尝试获取锁");
        if (Monitor.TryEnter(_lock, 7000))
        {
            try
            {
                Console.WriteLine($"Method3 | 7 秒内获取到锁");
            }
            finally
            {
                Console.WriteLine($"Method3 |开始释放锁");
                Monitor.Exit(_lock);
                Console.WriteLine($"Method3 |完成锁释放");
            }
        }
        else
        {
            Console.WriteLine($"Method3 | 7 秒内未获取到锁,自动退出锁");
        }
    }
}
public static void LockTryEnterRun()
{
    var example = new LockTryEnterExample();
    var thread1 = new Thread(example.Method1);
    var thread2 = new Thread(example.Method2);
    var thread3 = new Thread(example.Method3);
    thread1.Start();
    thread2.Start();
    thread3.Start();
}

执行结果如下:

可以发现当Method1锁定5秒后,Method2尝试3秒内获取锁,结果并未获取到自动退出;然后Method3尝试7秒内获取锁,结果获取到锁并正确释放锁。

04、实现生产者-消费者模式

除了上面介绍的方法,Monitor类还有Wait、Pulse、PulseAll等方法。

Wait: 该方法用于将当前线程放入等待队列,直到收到其他线程的信号通知。

Pulse: 该方法用于唤醒等待队列中的一个线程。当一个线程调用 Pulse 时,它会通知一个正在等待该对象锁的线程继续执行。

PulseAll: 该方法用于唤醒等待队列中的所有线程。

然后我们利用Monitor类的这些功能来实现一个简单的生产者-消费者模式。大致思路如下:

1.首先启动生产者线程,获取锁,然后生成数据;

2.当生产者生产的数据小于数据队列长度,则生产一条数据同时通知消费者线程进行消费,否则暂停当前线程等待消费者线程消费数据;

3.然后启动消费者线程,获取锁,然后消费数据;

4.当数据队列中有数据,则消费一条数据同时通知生产者线程可以生产数据了,否则暂停当前线程等待生产者线程生产数据;

具体代码如下:

csharp 复制代码
public class LockProducerConsumerExample
{
    private static Queue<int> queue = new Queue<int>();
    private static object _lock = new object();
    //生产者
    public  void Producer()
    {
        while (true)
        {
            lock (_lock)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                if (queue.Count < 3)
                {
                    var item = new Random().Next(100);
                    queue.Enqueue(item);
                    Console.WriteLine($"生产者,生产: {item}");
                    //唤醒消费者
                    Monitor.Pulse(_lock);  
                }
                else
                {
                    //队列满时,生产者等待
                    Console.WriteLine($"队列已满,生产者等待中......");
                    Monitor.Wait(_lock);  
                }
            }
            Thread.Sleep(500);
        }
    }
    // 消费者
    public  void Consumer()
    {
        while (true)
        {
            lock (_lock)
            {
                Console.ForegroundColor = ConsoleColor.Blue;
                if (queue.Count > 0)
                {
                    var item = queue.Dequeue();
                    Console.WriteLine($"消费者,消费: {item}");
                    //唤醒生产者
                    Monitor.Pulse(_lock);  
                }
                else
                {
                    //队列空时,消费者等待
                    Console.WriteLine($"队列已空,消费者等待中......");
                    Monitor.Wait(_lock);  
                }
            }
            Thread.Sleep(10000);
        }
    }
}
public static void LockProducerConsumerRun()
{
    var example = new LockProducerConsumerExample();
    var thread1 = new Thread(example.Producer);
    var thread2 = new Thread(example.Consumer);
    thread1.Start();
    thread2.Start();
    thread1.Join();
    thread2.Join();
}

执行结果如下:

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

相关推荐
鲤籽鲲3 天前
C# ManualResetEvent 类 使用详解
java·开发语言·c#·多线程
gp1033 天前
iOS主要知识点梳理回顾-2-多线程
ios·多线程·gcd
IT规划师4 天前
并发编程 - 线程同步(六)之锁lock
多线程·并发编程·线程同步
IT规划师5 天前
并发编程 - 线程同步(五)之原子操作Interlocked详解二
多线程·并发编程·线程同步
IT规划师7 天前
并发编程 - 线程同步(四)之原子操作Interlocked详解一
多线程·并发编程·线程同步
IT规划师8 天前
并发编程 - 线程同步(三)之原子操作Interlocked简介
多线程·并发编程·线程同步
charlie1145141919 天前
高阶开发基础——快速入门C++并发编程6——大作业:实现一个超级迷你的线程池
开发语言·c++·并发编程·基础学习
@Java小牛马10 天前
Redis真的是单线程的吗?
数据库·redis·缓存·reactor·单线程·多线程
桦说编程11 天前
CompletableFuture 超时功能有大坑!使用不当直接生产事故!
java·性能优化·函数式编程·并发编程