c# 并发与异步 五、排它锁

本文详细介绍了C#中的锁机制,包括lock语句、Mutex和SpinLock的使用,强调了线程安全的重要性。通过示例展示了如何避免死锁和确保原子性操作。同时,探讨了何时使用锁以及如何避免死锁,提出了使用一致的顺序锁定对象的建议,以降低死锁风险。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、排它锁的结构

        排它锁结构有三种:lock语句、Mutex和SpinLock。

        其中lock是最方便,最常用的结构。而其他两种结构多用于处理特定的情形。

        Mutex可以跨越多个进程(计算机范围锁)。

        SpinLock可用于实现微优化。它可以在高并发场景下减少上下文切换。

二、lock语句

        如下的类,这个类不是线程安全的,如果方法被两个线程同时调用,可能会出现被零除的错误,因为 _para2 可以在一个线程中设置为零,而另一个线程正在执行之间 if 语句和 Console.WriteLine。。

class ThreadUnsafe
{
  static int _para1 = 1, _para2 = 1;
 
  static void M_Method()
  {
    if (_para2!= 0) Console.WriteLine (_para1 / _para2);
    _para2= 0;
  }
}

        加锁保证线程安全,一次只有一个线程可以锁定同步对象(在本例中为 _locker),并且任何竞争线程都会被阻塞,直到锁被释放。

class ThreadSafe
{
  static readonly object _locker = new object();
  static int _para1, _para2;
 
  static void M_Method()
  {
    lock (_locker)
    {
      if (_para2 != 0) Console.WriteLine (_para1 / _para2);
      _para2 = 0;
    }
  }
}

        同步对象

        若一个对象在各个参与线程中都是可见的,那么该对象就可以作为同步对象。但是该对象必须是一个引用类型的对象(这是必须满足的条件)。同步对象通常是私有的(因为这样便于封装锁逻辑),而且一般是实例字段或者静态字段。 

三、Monitor.Enter and Monitor.Exit

        C#的lock语句是包裹在try/finally语句块中的Monitor.Enter和Monitor.Exit语法糖。调用 Monitor.Exit 而不先调用同一对象上的 Monitor.Enter 会引发异常。

Monitor.Enter (_locker);
try
{
  if (_para2 != 0) Console.WriteLine (_para1 / _para2);
  _para2 = 0;
}
finally { Monitor.Exit (_locker); }

        如果上述try方法块内出现异常,则锁的状态是不确定的。所以后面语言版本更新后推出了更加健壮的版本,lock语句将会翻译为以下模式。

bool lockTaken = false;
try
{
  Monitor.Enter (_locker, ref lockTaken);
  // Do your stuff...
}
finally { if (lockTaken) Monitor.Exit (_locker); }

        TryEnter

        Monitor还提供了TryEnter方法来指定一个超时时间(以毫秒为单位的整数或者一个TimeSpan值)。如果在指定时间内获得了锁,则该方法返回true,如果超时并且没有获得锁,该方法返回false。如果不给TryEnter方法提供任何参数,且当前无法获得锁,则该方法会立即超时。和Enter方法一样,TryEnter方法也在CLR 4.0中进行了重载,并在重载中接受lockTaken参数。

四、什么时候使用锁

        使用锁的基本原则是:若需要访问可写的共享字段,则需要在其周围加锁。即便对于最简单的情况(例如对某个字段进行赋值),也必须考虑进行同步。

        非线程安全

class ThreadUnsafe
{
  static int _x;
  static void AddM() { _x++; }
  static void SetM()    { _x = 123; }
}

        线程安全

class ThreadSafe
{
  static readonly object _locker = new object();
  static int _x;
 
  static void AddM() { lock (_locker) _x++; }
  static void SetM()    { lock (_locker) _x = 123; }
}

五、原子性

        如果使用同一个锁对一组变量的读写操作进行保护,那么可以认为这些变量的读写操作是原子的(atomically)。假设我们只在locker锁中对x和y字段进行读写:

lock (locker) { if (x != 0) y /= x; }

        则可以称x和y是以原子方式访问的。因为上述代码块是无法分割执行的,也不可能被其他能够更改x和y的值,或是破坏其输出结果的线程抢占。因此只要x和y永远在相同的排它锁中进行访问,那么上述代码就永远不会发生除数为零的错误。

六、嵌套锁

        线程可以用嵌套(重入)的方式重复锁住同一个对象。

lock (locker)
  lock (locker)
    lock (locker)
    {
       // Do something...
    }

Monitor.Enter (locker); Monitor.Enter (locker);  Monitor.Enter (locker); 
// Do something...
Monitor.Exit (locker);  Monitor.Exit (locker);   Monitor.Exit (locker);

        在这些情况下,只有当最外层的 lock 语句退出或匹配数量的 Monitor.Exit 语句已执行时,对象才会解锁。

        当一个方法在锁中调用另一个方法时,嵌套锁定很有用:

static readonly object _locker = new object();
 
static void Method1()
{
  lock (_locker)
  {
     Method2();
     // 我们仍然有锁——因为锁是可重入的。
  }
}
 
static void Method2()
{
  lock (_locker) { Console.WriteLine ("method2"); }
}

七、死锁

        两个线程互相等待对方占用的资源就会使双方都无法继续执行,从而形成死锁。演示死锁的最简单的方法是使用两个锁。

object locker1 = new object();
object locker2 = new object();
 
new Thread (() => {
                    lock (locker1)
                    {
                      Thread.Sleep (1000);
                      lock (locker2);      // Deadlock
                    }
                  }).Start();
lock (locker2)
{
  Thread.Sleep (1000);
  lock (locker1);                          // Deadlock
}

        标准托管环境下的CLR和SQL Server不同,它不会自动检查和处理死锁(强制终止其中一个线程)。除非指定超时时间,否则线程死锁将致使线程永久阻塞。(SQL Server的CLR集成托管环境则不同,它会自动检测死锁,然后在其中的一个线程抛出一个可捕获的异常。)。

        为了尽可能避免死锁,最常见的建议是“使用一致的顺序锁定对象以避免死锁”。另一个更好的方式是,当锁定一个对象的方法调用时,务必警惕该对象是否持有当前对象的引用。

        使用更高级的同步手段,例如任务的延续/组合器、数据并行、不可变类型都可以减少对锁的依赖。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

坐望云起

如果觉得有用,请不吝打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值