聊透多线程编程-线程互斥与同步-12. C# Monitor类实现线程互斥

目录

一、什么是临界区?

二、Monitor类的用途

三、Monitor的基本用法

四、Monitor的工作原理

五、使用示例1-保护共享变量

解释:

六、使用示例2-线程间信号传递

解释:

七、注意事项

八、总结


 

在多线程编程中,线程之间的同步和互斥是确保程序正确运行的关键。C# 提供了多种机制来实现线程同步,其中 Monitor 类是一个底层但功能强大的工具,用于实现线程间的互斥访问。本文将详细介绍如何使用 Monitor 类实现线程互斥,并通过示例展示其工作原理。


一、什么是临界区?

在多线程编程中,临界区是指一段需要互斥访问的代码块,通常涉及对共享资源的操作。为了避免多个线程同时操作共享资源而导致数据竞争或状态不一致,我们需要对临界区代码进行保护。

例如,如果两个线程同时修改一个共享变量,可能会导致最终结果不符合预期。因此,我们需要一种机制来确保同一时间只有一个线程可以进入临界区。


二、Monitor类的简介

Monitor 类是 .NET 提供的一个低级线程同步工具,主要用于实现线程间的互斥访问(Monitor 实现的线程互斥主要是一种软件实现方法,但并不是基于经典的线程同步算法如单标志法、双标志法或 Peterson 方法来实现的,而是基于现代操作系统和硬件提供的更高级别的同步原语(如原子操作和内核对象)构建的,这是因为单标志法、双标志法和 Peterson 方法都依赖于轮询(busy-waiting),这会导致 CPU 资源浪费,不适合现代多处理器环境)。与 lock 关键字不同,Monitor 提供了更细粒度的控制能力,允许开发者手动管理锁的获取和释放。

Monitor 的主要方法包括:

  • Monitor.Enter(object):尝试获取指定对象的锁。如果锁已被占用,则当前线程会被阻塞,直到锁被释放。
  • Monitor.TryEnter(object):尝试获取指定对象的锁,但不会无限期阻塞。它返回一个布尔值,指示是否成功获取锁。可以通过重载版本指定超时时间(以毫秒为单位),如果在指定时间内未能获取锁,则返回 false。
  • Monitor.Exit(object):释放指定对象的锁。
  • Monitor.Wait(object):释放锁并使当前线程进入等待状态,直到其他线程调用 Monitor.Pulse 或 Monitor.PulseAll。
  • Monitor.Pulse(object):通知等待队列中的一个线程继续执行。
  • Monitor.PulseAll(object):通知等待队列中的所有线程继续执行。

三、Monitor的基本用法

Monitor 的基本用法类似于 lock,但需要手动调用 EnterExit 方法。以下是一个简单的例子:

private static readonly object _lock = new object(); // 锁对象

Monitor.Enter(_lock);
try
{
    // 需要同步的代码块
}
finally
{
    Monitor.Exit(_lock); // 确保锁一定会被释放
}
  • _lock 是一个引用类型的对象,作为锁的标识。
  • 使用 try-finally 块是为了确保即使发生异常,锁也能被正确释放。

四、Monitor的工作原理

Monitor 类的核心思想是基于锁对象的互斥机制:

  1. 当线程调用 Monitor.Enter(object) 时,它会尝试获取指定对象的锁。如果锁已被其他线程占用,则当前线程会被挂起,直到锁可用。
  2. 当线程调用 Monitor.Exit(object) 时,它会释放锁,允许其他线程获取该锁。
  3. Monitor.Wait 和 Monitor.Pulse 则用于实现线程间的信号传递,允许线程在特定条件下暂停或恢复执行。

五、使用示例1-保护共享变量

下面是一个使用 Monitor 类保护共享变量的例子:

using System;
using System.Threading;

class Program
{
    private static int _counter = 0;
    private static readonly object _lock = new object();

    static void Main()
    {
        Thread t1 = new Thread(IncrementCounter);
        Thread t2 = new Thread(IncrementCounter);

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine($"Final Counter Value: {_counter}");
    }

    static void IncrementCounter()
    {
        for (int i = 0; i < 100000; i++)
        {
            Monitor.Enter(_lock);
            try
            {
                _counter++;
            }
            finally
            {
                Monitor.Exit(_lock);
            }
        }
    }
}

解释:

  • _lock 是一个静态对象,用于标识锁。
  • 每次访问 _counter 时,都会通过 Monitor.Enter 获取锁,并通过 Monitor.Exit 释放锁。
  • 最终输出的结果是 200000,因为所有线程的操作都被正确同步了。

六、使用示例2-线程间信号传递

Monitor 类还可以用于实现线程间的信号传递。以下是一个典型的生产者-消费者模型示例:

using System;
using System.Threading;

class Program
{
    private static readonly object _lock = new object();
    private static bool _isReady = false; // 共享的状态变量

    static void Main(string[] args)
    {
        Thread threadA = new Thread(DoWorkA);
        Thread threadB = new Thread(DoWorkB);

        threadA.Start();
        threadB.Start();

        threadA.Join();
        threadB.Join();
    }

    static void DoWorkA()
    {
        lock (_lock)
        {
            Console.WriteLine("Thread A: Waiting for signal...");

            // 等待条件满足
            while (!_isReady)
            {
                Monitor.Wait(_lock); // 释放锁并等待
            }

            Console.WriteLine("Thread A: Received signal. Continuing work.");
        }
    }

    static void DoWorkB()
    {
        Thread.Sleep(2000); // 模拟一些工作

        lock (_lock)
        {
            Console.WriteLine("Thread B: Preparing to signal...");

            // 设置条件为 true
            _isReady = true;

            // 通知等待的线程
            Monitor.Pulse(_lock);
            Console.WriteLine("Thread B: Signal sent.");
        }
    }
}

解释:

  • 线程 A 调用 Monitor.Wait 释放锁并进入等待状态,直到线程 B 调用 Monitor.Pulse 通知它继续执行。
  • 这种机制非常适合需要线程间协作的场景。

七、注意事项

  1. 锁对象的选择

    • 锁对象必须是引用类型(如 object),不能是值类型(如 int)。
    • 推荐使用专用的私有对象作为锁对象,避免与其他代码发生冲突。
    • 不要使用 this 或字符串常量作为锁对象,以免引发死锁。
  2. 避免死锁

    • 死锁是指多个线程互相等待对方释放锁,导致程序无法继续运行。
    • 避免死锁的方法包括:
      • 确保锁的获取顺序一致。
      • 尽量减少锁的范围。
  3. 性能影响

    • 使用 Monitor 会导致线程阻塞,从而影响性能。
    • 对于高并发场景,可以考虑使用其他同步机制(如 SemaphoreSlim 或 ReaderWriterLockSlim)。

八、总结

Monitor 类是 C# 中实现线程互斥的一种重要工具,提供了比 lock 更灵活的控制能力。尽管它的使用稍微复杂一些,但能够满足更多高级需求,例如线程间的信号传递。

在实际开发中,选择合适的同步机制非常重要。对于简单的线程互斥场景,lock 可能更为直观;而对于需要更细粒度控制的场景,Monitor 则是一个不错的选择。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值