C# lock 和 Monitor

lock

lock 关键字是 C# 中最常用的线程同步工具之一,它使得一段代码在同一时间只能被一个线程执行,以确保对共享资源的访问不会被其他线程干扰。

lock 关键字的语法如下:

lock (lockObject)
{
    // 要同步的代码块
}
  • lockObject:是一个对象实例,用于定义临界区域。多个线程在执行 lock 代码块时,如果它们尝试获取相同的 lockObject 锁,则只有一个线程能够成功获取锁,其他线程将被阻塞,直到锁被释放。
  • lock 关键字的优点是简单易用,编译器会自动处理锁的获取和释放,使得代码更加清晰和简洁。
  • lock 关键字的缺点是在锁的范围内不能发生阻塞,否则会导致死锁。此外,由于 lock 是语言级别的关键字,所以无法对它进行更细粒度的控制。

在 C# 中,lock 关键字实际上是对 Monitor 类的一种简化用法,它会被编译器转换成 Monitor 类的方法调用。当你使用 lock 关键字时,编译器会自动创建一个 Monitor 对象,并在代码块的开始处调用 Monitor.Enter 方法获取锁,在代码块的结束处调用 Monitor.Exit 方法释放锁。

Monitor

Monitor 类提供了细粒度的线程同步控制,它允许程序员手动管理同步块的进入和退出,并且支持更复杂的线程同步操作。

Monitor 类的基本用法如下:

// 进入同步代码块
Monitor.Enter(lockObject);
try
{
    // 同步代码块
}
finally
{
    // 退出同步代码块
    Monitor.Exit(lockObject);
}
  • Monitor 类需要使用 EnterExit 方法手动管理同步块的进入和退出。Enter 方法用于进入同步代码块,Exit 方法用于退出同步代码块。通常会在 try-finally 块中使用,以确保锁的释放。
  • Monitor 类还提供了 WaitPulsePulseAll 方法,用于线程之间的通信和同步。Wait 方法用于让线程等待,Pulse 方法用于唤醒等待的线程,PulseAll 方法用于唤醒所有等待的线程。
  • Monitor 类的优点是可以手动控制同步块的进入和退出,以及支持更复杂的线程同步操作,比如等待和唤醒线程。
  • Monitor 类的缺点是使用起来相对复杂,需要手动管理锁的获取和释放,容易出现遗漏或错误,导致死锁或其他线程同步问题。

案例

分享一个简单的火车票购票系统的例子,来说明  lock 关键字的使用方式以及与Monitor 类的对比。

假设有一个火车票购票系统,多个用户同时在线购票。我们希望在多线程环境中正确管理火车票的分配,避免出现超卖或者座位重复分配的情况。

使用lock

创建一个 TicketSystem 类来管理座位的状态和购票操作:

using System;
using System.Threading;

public class TicketSystem
{
    private object lockObject = new object(); // 用于同步的锁对象
    private int availableSeats; // 可用座位数量

    public TicketSystem(int totalSeats)
    {
        availableSeats = totalSeats;
    }

    public void BuyTicket(string customerName, int numSeats)
    {
        lock (lockObject)
        {
            if (availableSeats >= numSeats)
            {
                // 模拟购票操作
                Console.WriteLine($"{customerName} 购买了 {numSeats} 张票。");
                availableSeats -= numSeats;
            }
            else
            {
                Console.WriteLine($"对不起,{customerName},没有足够的座位。");
            }
        }
    }
}


class Program
{
    static void Main(string[] args)
    {
        TicketSystem ticketSystem = new TicketSystem(15); // 总共15个座位

        Thread[] threads = new Thread[20]; // 20个线程模拟20个用户

        for (int i = 0; i < 20; i++)
        {
            threads[i] = new Thread(() =>
            {
                string customerName = $"User {Thread.CurrentThread.ManagedThreadId}";
                ticketSystem.BuyTicket(customerName, 1);
            });
        }

        // 启动所有线程
        foreach (var thread in threads)
        {
            thread.Start();
        }

        // 等待所有线程执行完成
        foreach (var thread in threads)
        {
            thread.Join();
        }

        Console.WriteLine("所有用户购票结束。");
        Console.ReadKey();
    }
}

使用Monitor 

using System;
using System.Threading;

public class TicketSystem
{
    private object lockObject = new object(); // 用于同步的锁对象
    private int availableSeats; // 可用座位数量

    public TicketSystem(int totalSeats)
    {
        availableSeats = totalSeats;
    }

    public void BuyTicket(string customerName, int numSeats)
    {
        Monitor.Enter(lockObject);
        try
        {
            if (availableSeats >= numSeats)
            {
                // 模拟购票操作
                Console.WriteLine($"{customerName} 购买了 {numSeats} 张票。");
                availableSeats -= numSeats;
            }
            else
            {
                Console.WriteLine($"对不起,{customerName},没有足够的座位。");
            }
        }
        finally
        {
            Monitor.Exit(lockObject);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        TicketSystem ticketSystem = new TicketSystem(15); // 总共15个座位

        Thread[] threads = new Thread[20]; // 20个线程模拟20个用户

        for (int i = 0; i < 20; i++)
        {
            threads[i] = new Thread(() =>
            {
                string customerName = $"User {Thread.CurrentThread.ManagedThreadId}";
                ticketSystem.BuyTicket(customerName, 1);
            });
        }

        // 启动所有线程
        foreach (var thread in threads)
        {
            thread.Start();
        }

        // 等待所有线程执行完成
        foreach (var thread in threads)
        {
            thread.Join();
        }

        Console.WriteLine("所有用户购票结束。");
        Console.ReadKey();
    }
}

上面的场景中使用lock和Moniter效果都是相同的,接下来描述lock 没有的功能。Monitor的Wait和Pulse 方法

Monitor的Wait和Pulse

我们知道春节火车票不是一次性放票的而是分段开放的,同时也有退票的情况。这将导致余票的数量动态变化。多个用户线程同时等待抢票,抢票系统一旦检测到有新的余票释放,将通知所有等待的线程进行抢票。这种情况下,我们需要使用线程同步机制确保多个线程之间的正确操作,并及时通知用户线程有新票可供抢购。

using System;
using System.Threading;

public class TicketSystem
{
    private int availableTickets = 0;
    private object lockObject = new object();

    public TicketSystem(int initialTickets)
    {
        this.availableTickets = initialTickets;
    }

    public void SellTicket()
    {
        Monitor.Enter(lockObject);
        try
        {
            while (availableTickets == 0)
            {
                Console.WriteLine($"{Thread.CurrentThread.Name} is waiting for available tickets...");
                Monitor.Wait(lockObject);
            }

            // Simulate selling ticket
            availableTickets--;
            Console.WriteLine($"{Thread.CurrentThread.Name} sold a ticket. Available tickets: {availableTickets}");
        }
        finally
        {
            Monitor.Exit(lockObject);
        }
    }

    public void AddTickets(int numTicketsToAdd)
    {
        Monitor.Enter(lockObject);
        try
        {
            // Simulate adding new tickets
            availableTickets += numTicketsToAdd;
            Console.WriteLine($"Added {numTicketsToAdd} new tickets. Available tickets: {availableTickets}");

            // Notify waiting threads
            Monitor.PulseAll(lockObject);
        }
        finally
        {
            Monitor.Exit(lockObject);
        }
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        TicketSystem ticketSystem = new TicketSystem(0); // 初始没有票

        // 创建并启动5个抢票线程
        for (int i = 0; i < 5; i++)
        {
            Thread thread = new Thread(() =>
            {
                while (true)
                {
                    ticketSystem.SellTicket();
                    Thread.Sleep(1000); // 模拟抢票过程
                }
            });
            thread.Name = $"TicketBuyer {i + 1}";
            thread.Start();
        }

        // 模拟分段放票
        for (int i = 0; i < 3; i++)
        {
            Thread.Sleep(3000); // 等待3秒模拟间隔放票
            ticketSystem.AddTickets(5); // 每次放5张票
        }

        Console.WriteLine("Press any key to exit...");
        Console.ReadKey();
    }
}

这段代码实现了一个简单的抢票功能,具体功能和实现方式如下:

  • 使用 TicketSystem 类管理票务信息,包括余票数量和售票方法。
  • Main 方法中创建多个线程模拟不同的抢票者,它们会不断尝试购买票。
  • 使用 Monitor.EnterMonitor.Exit 实现线程同步,确保多线程访问临界区时的安全。
  • 使用 Monitor.WaitMonitor.PulseAll 实现线程间的通信,唤醒等待的线程。
  • 程序模拟分段开放票务,定时增加新的票。

由于 lock 示例本质上是实现了Monitor,所以也可以使用lock + Monitor.Wait 和 Monitor.PulseAll 方式使用。

public class TicketSystem
{
    private int availableTickets = 0;

    public TicketSystem(int initialTickets)
    {
        this.availableTickets = initialTickets;
    }

    public void SellTicket()
    {
        lock (this)
        {
            while (availableTickets == 0)
            {
                Console.WriteLine($"{Thread.CurrentThread.Name} is waiting for available tickets...");
                Monitor.Wait(this);
            }

            // Simulate selling ticket
            availableTickets--;
            Console.WriteLine($"{Thread.CurrentThread.Name} sold a ticket. Available tickets: {availableTickets}");
        }
    }

    public void AddTickets(int numTicketsToAdd)
    {
        lock (this)
        {
            // Simulate adding new tickets
            availableTickets += numTicketsToAdd;
            Console.WriteLine($"Added {numTicketsToAdd} new tickets. Available tickets: {availableTickets}");

            // Notify waiting threads
            Monitor.PulseAll(this);
        }
    }
}

缺点

  1. 性能开销
    • Monitor 是一种内核模式构造,它提供了互斥锁功能,用于保护共享资源免受并发访问的影响。
    • 但是,每次在 Monitor 上调用 Enter Exit 方法时,都需要进行用户模式到内核模式的切换,这会导致性能开销。
    • 如果应用程序中频繁使用 Monitor,这些切换可能会显著影响整体性能。
  1. 死锁风险
    • Monitor 的使用需要谨慎,因为不正确的使用可能导致死锁。
    • 如果线程在持有锁的情况下等待其他资源,而其他资源又在等待相同的锁,就会发生死锁。
    • 死锁是一种严重的问题,会导致应用程序停滞不前。
posted @ 2024-04-07 18:29  咸鱼翻身?  阅读(517)  评论(0编辑  收藏  举报