C#线程:详解互斥锁
排他锁结构有三种:lock语句
、Mutex
和SpinLock
。
其中lock是最方便最常用的结构。而其他两种结构多用于处理特定的情形:Mutex可以跨越多个进程(计算机范围锁)。SpinLock可用于实现微优化,可以在高并发场景下减少上下文切换。
lock语句
先看如下代码:
class ThreadUnsafe
{
static int _val1 = 1, _val2 = 1;
static void Go()
{
if (_val2 != 0)
Console.WriteLine(_val1 / _val2);
_val2 = 0;
}
}
以上的类不是线程安全的。如果两个线程同时调用Go方法,则有可能出现除数为0的错误。因为_val2有可能被第一个线程设置为0,而第二个线程正处于if和Console.WriteLine语句之间。下例使用了lock来修正这个错误:
class ThreadSafe
{
static readonly object _locker = new object();
static int _val1 = 1, _val2 = 1;
static void Go()
{
lock (_locker)
{
if (_val2 != 0)
Console.WriteLine(_val1 / _val2);
_val2 = 0;
}
}
}
每一次只能有一个线程锁定同步对象_locker,而其他线程则被阻塞,直至锁释放。如果参与竞争的线程多于一个,则它们需要在准备队列中排队,并以先到先得的方式获得锁。排他锁会强制以所谓序列的方式访问被锁保护的资源,因为线程之间的访问是不能重叠的。因此,本例中的锁保护了Go方法中的访问逻辑,也保护了_val1和_val2字段。
Monitor.Enter方法和Monitor.Exit方法
C#的lock语句是包裹在try/finally语句块中的Monitor.Enter
和Monitor.Exit
语法糖,因此上例Go方法的实际操作为(以下代码对部分逻辑进行了简化):
Monitor.Enter(_locker);
try
{
if (_val2 != 0)
Console.WriteLine(_val1 / _val2);
_val2 = 0;
}
finally
{
Monitor.Exit(_locker);
}
如果调用Monitor.Exit之前并没有对同一个对象调用Monitor.Enter,则该方法会抛出异常。
lockTaken重载
上述示例代码中有一个不易发现的漏洞。如果在Monitor.Enter和try语句块之间抛出了(很少见)异常,那么锁的状态是不确定的。但若已经获得了锁,那么这个锁就永远无法释放,因为已经没有机会进入try/finally代码块了。因此这种情况会造成锁泄露。为了防范这种风险,Monitor.Enter进行了如下重载。
Enter方法执行结束后,当且仅当该方法执行时抛出了异常且没有获得锁时,lockTaken为false。
bool lockTaken = false;
try
{
Monitor.Enter(_locker, ref lockTaken);
if (_val2 != 0)
Console.WriteLine(_val1 / _val2);
_val2 = 0;
}
finally
{
if(lockTaken)
Monitor.Exit(_locker);
}
TryEnter
Monitor还提供了TryEnter方法来指定一个超时时间(以毫秒为单位的整数或者一个TimeSpan值)。如果在指定时间内获得了锁,则该方法返回true,如果超时并且没有获得锁,该方法返回false。如果不给TryEnter方法提供任何参数,且当前无法获得锁,则该方法会立即超时。和Enter方法一样,TryEnter方法也进行了重载,并在重载中接受lockTaken参数。
选择同步对象
若一个对象在各个参与线程中都是可见的,那么该对象就可以作为同步对象。但是该对象必须是一个引用类型的对象(这是必须满足的条件)。同步对象通常是私有的(因为这样便于封装锁逻辑),而且一般是实例字段或者静态字段。
同步对象本身也可以是被保护的对象,如下面_list。
List<string> _list = new List<string>();
void Test()
{
lock(_list){_list.Add("aaa")}
...
}
如果一个字段仅作为锁存在(如前一节中的_locker),则可以精确地控制锁的范围和粒度。
除此之外,Lambda表达式或匿名方法中捕获的局部变量也可以作为同步对象进行锁定。
使用锁的时机
使用锁的基本原则是:若需要访问可写的共享字段,则需要在其周围加锁。即便对于最简单的情况(例如对某个字段进行赋值),也必须考虑进行同步。
以下示例中的Increment和Assign方法,不是线程安全的和线程安全的写法:
// 不是线程安全
class TreadUnsafe
{
static int _x;
static void Increment() { _x++; }
static void Assign() { _x = 123; }
}
// 线程安全
class ThreadSafe
{
static readonly object _locker = new object();
static int _x;
static void Increment() { lock(_locker) _x++; }
static void Assign() { lock (_locker) _x = 123; }
}
锁与原子性
如果使用同一个锁对一组变量的读写操作进行保护,那么可以认为这些变量的读写操作是原子的。
假设我们只在locker锁中对x和y字段进行读写:lock(locker) { if(x!=0) y/=x; }
则可以称x和y是以原子方式访问的。
因为上述代码块是无法分割执行的,也不可能被其他能够更改x和y的值的且破坏其输出结果的线程抢占。因此只要x和y永远在相同的排他锁中进行访问,那么上述代码就永远不会发生除数为零的错误。
嵌套锁
线程可以用嵌套(重入)的方式重复锁住同一个对象:
lock(locker)
lock(locker)
lock(locker)
{ ... }
在使用嵌套锁时,只有最外层的lock语句退出时(或者执行相同数目的Monitor.Exit时)对象的锁才会解除。
当锁中的方法调用另一个方法时,嵌套锁很奏效,线程只会阻塞在第一个(最外层的)锁上。
static readonly object _locker = new object();
static void Main()
{
lock (_locker)
{
AnotherMethod();
}
}
static void AnotherMethod()
{
lock (_locker) { Console.WriteLine("Another method"); }
}
死锁
两个线程互相等待对方占用的资源就会使双方都无法继续执行,从而形成死锁。
演示死锁的最简单的方法是使用两个锁:
object locker1 = new object();
object locker2 = new object();
new Thread(() =>
{
lock (locker1)
{
Thread.Sleep(1000);
lock (locker2) ;
}
}).Start();
lock (locker2)
{
Thread.Sleep(1000);
lock (locker1) ;
}
死锁是多线程中最难解决的问题之一,尤其是当其涉及了很多相互关联的对象时。而其中最难的部分是确定调用者持有了哪些锁。
当锁定一个对象的方法调用时,务必警惕该对象是否可能持有当前对象的引用。此外,请确认是否真正有必要在调用其他类的方法时添加锁。
上一篇: 全面解析:MySQL中的锁机制
推荐阅读
-
C#在WinForm中进行多线程开发的第一步:Thread类库详解
-
C#线程:详解互斥锁
-
Java多线程实战(十二): 明白的锁机制详解
-
Linux线程管理必备:解析互斥量与条件变量的详解
-
Java多线程中的锁机制详解
-
详解各类锁机制:互斥锁、条件变量、读写锁与自旋锁的工作原理及其理解
-
深入理解协程与互斥锁:KotlinMutex的实战详解
-
理解Java文件锁FileLock在多线程环境下的工作机制详解
-
【2022新手指南】Java编程进阶之路 - 六、技术架构篇 ### MySQL索引底层解析与优化实战 - 你会讲解MySQL索引的数据结构吗?性能调优技巧知多少? - Redis深度揭秘:你知道多少?从基础到哨兵、主从复制全梳理 - Redis持久化及哨兵模式详解,还有集群搭建和Leader选举黑箱打开 - Zookeeper是个啥?特性和应用场景大公开 - ZooKeeper集群搭建攻略及 Leader选举、读写一致性、共享锁实现细节 - 探究ZooKeeper中的Leader选举机制及其在分布式环境中的作用 - Zab协议深入剖析:原理、功能与在Zookeeper中的核心地位 - RabbitMQ全方位解读:工作模式、消费限流、可靠投递与配置策略 - 设计者视角:RabbitMQ过期时间、死信队列与延时队列实践指南 - RocketMQ特性和应用场景揭示:理解其精髓与差异化优势 - Kafka详细介绍:特性及广泛应用于实时数据处理的场景解析 - ElasticSearch实力揭秘:特性概述与作为搜索引擎的广泛应用 - MongoDB认知升级:非关系型数据库的优势阐述,安装与使用实战教学 - BIO/NIO/AIO网络模型对比:掌握它们的区别与在网络编程中的实际应用 - Netty带你飞:理解其超快速度背后的秘密,包括线程模型分析 - 网络通信黑科技:Netty编解码原理与常用编解码器的应用,Protostuff实战演示 - 解密Netty粘包与拆包现象,怎样有效应对这一常见问题 - 自定义Netty心跳检测机制,轻松调整检测间隔时间的艺术 - Dubbo轻骑兵介绍:核心特性概览,服务降级实战与其实现益处 - Dubbo三大神器解读:本地存根与本地伪装的实战运用与优势呈现 ----------------------- 七、结语与回顾
-
用Linux多线程编程(C语言版):构建基于互斥锁的线程安全队列实例