C# 开发者 (面试者) 都会遇到 lock(Monitor),Mutex,Semaphore,SemaphoreSlim 这四个与锁相关的 C# 类型, 本文期望以最简洁明了的方式阐述四种对象的区别.
什么叫线程安全?
教条式理解
如果代码在多线程环境中运行的结果与 单线程运行结果一样, 其他变量值也和预期是一样的, 那么线程就是安全的;
线程不安全就是不提供数据访问保护, 可能出现多个线程先后修改数据造成的结果是脏数据.
实际场景理解
两个线程都为集合增加元素, 我们错误的理解即使是多线程也总有先后顺序吧, 集合的两个位置先后塞进去就完了; 实际上集合增加元素这个行为看起来简单, 实际并不一定是原子操作.
在添加一个元素的时候, 它可能会有两步来完成:
在 Items[Size] 的位置存放此元素;
增大 Size 的值.
在单线程运行的情况下, 如果 Size = 0, 添加一个元素后, 此元素在位置 0, 之后设置 Size=1;
如果是在多线程场景下, 有两个线程, 线程 A 先将元素存放在位置 0, 但是此时 CPU 调度线程 A 暂停, 线程 B 得到运行机会; 线程 B 也向此 ArrayList 添加元素, 因为此时 Size 仍然等于 0 (注意哦, 我们假设添加元素是经过两个步骤, 而线程 A 仅仅完成了步骤 1), 所以线程 B 也将元素存放在位置 0. 然后线程 A 和线程 B 都继续运行, 都增加 Size 的值. 那好, 我们来看看 ArrayList 的情况, 元素实际上只有一个, 存放在位置 0, 而 Size 却等于 2, 形成了脏数据, 这种就定义为对 ArrayList 的新增元素操作是线程不安全的.
线程安全这个问题不单单存在于集合类, 我们始终要记得:
Never ever modify a shared resource by multipie threads unless resource is thread-safe.
我们对 SqlServer,MongoDB, 对 HttpContext 的访问都会涉及 thread-safe, 利用 C# MongoDB driver 操作 Mongo 打包时常用操作是线程安全的, Only a few of the C# Driver classes are thread safe. Among them: MongoServer, MongoDatabase, MongoCollection and MongoGridFS.
对于 HttpContext 静态属性的操作是线程安全的: Any public static members of this type (HttpContext) are thread safe, any instance members are not guaranteed to be thread safe. 我们常用的是 HttpContext.Current
各语言推出了适用于不同范围的线程同步技术来预防以上脏数据(实现线程安全).
C# 线程同步技术
话不多说, 给出大图:
四象限对象的区别:
该线程同步技术
- 支持线程进入的个数
- 是否跨进程支持
其中
1 lock vs Monitor
最常用的 lock 关键字, 能在多线程环境下确保只有一个线程在执行 {被保护的代码}, 其他线程则必须等待进入的线程完成工作代码.
上图将 lock 和 Monitor 放在一起, 是因为 lock 是 Monitor 的语法糖, 实际的编译代码如下:
- bool lockTaken = false;
- try
- {
- Monitor.Enter(obj, ref lockTaken);
- //...
- }
- finally
- {
- if (lockTaken) Monitor.Exit(obj);
- }
- 2 lock(Monitor)vs Mutex(中文称为互斥锁, 互斥元)
lock/Monitor 维护进程内线程的安全性, Mutex 维护跨进程的线程安全性.
这 2 个对象都只支持单线程进入指定代码.
3 SemaphoreSlim vs Semaphore
中文都称为信号量, 根据对象初始化的配置, 能够允许单个或多个线程进入保护代码.
信号量使多个并发线程可以访问共享资源(最大为您指定的最大数量), 当线程请求访问资源时, 信号量计数递减, 而当它们释放资源时, 信号量计数又递增.
SemaphoreSlim 是一个轻量级的, 由 CRL 支持的进程内信号量.
右侧 Mutex 和 Semaphore 都是内核对象, 可以看到他们都继承自 WaitHandle 对象,
左侧 Monitor,SemaphoreSlim 是. NET CLR 对象,
4 Monitor vs SemaphoreSlim
两者都是进程内线程同步技术, SemaphoreSlim 信号量支持多线程进入;
另外 SemaphoreSlim 有异步等待方法, 支持在异步代码中线程同步, 能解决在 async code 中无法使用 lock 语法糖的问题;
- // 实例化单信号量
- static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1,1);
- // 异步等待进入信号量, 如果没有线程被授予对信号量的访问权限, 则进入执行保护代码; 否则此线程将在此处等待, 直到信号量被释放为止
- await semaphoreSlim.WaitAsync();
- try
- {
- await Task.Delay(1000);
- }
- finally
- {
- // 任务准备就绪后, 释放信号灯.[准备就绪时始终释放信号量] 至关重要, 否则我们将获得永远被锁定的信号量
- // 这就是为什么在 try ... finally 子句中进行发布很重要的原因; 程序执行可能会崩溃或采用其他路径, 这样可以保证执行
- semaphoreSlim.Release();
- }
从目前看跨进程线程同步很少见, 倒是分布式锁越来越多常见了.
总结:
文章没有讲述每个对象使用方式, 自行看 MSDN doc. 从象限图中快速知晓 这 4 种线程同步技术的区别:
- 是否支持跨进程线程同步
- 是否支持多线程进入被保护代码.
从宏观上了解四个对象的区别对于[线程同步] 知识体系的形成是有帮助的.
来源: https://www.cnblogs.com/JulianHuang/p/11765397.html