目录

1.lock

2.Monitor

3.锁的其它要注意的问题

3.1同步对象的选择

3.2什么时候该上锁

3.3锁和原子性

3.4嵌套锁

3.5 死锁

3.6 性能

4.Mutex

5.Semaphore


1.lock

让我们先看一段代码:

class ThreadUnsafe
{static int _val1 = 1, _val2 = 1;static void Go(){if (_val2 != 0) Console.WriteLine (_val1 / _val2);_val2 = 0;}
}

        这段代码在单线程运行时是安全的,但是多线程运行就会出现问题,即有可能在做除法时,出现_val2=0 的情况,这是由于在执行打印时,另一个线程可能会去修改_val2的值。

我们可以通过加锁来解决这个问题:

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

关键字lock保证任何时候,只有一个线程可以访问变量_val1 _val2

2.Monitor

        关键字lock的机制是靠Monitor类实现的。lock可以认为是利用try-finally 结构对Monitor.Enter和Monitor.Exit 函数进行封装。比如上面使用lock关键字的代码用Monitor类实现如下:

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

(ps:在没有调用Monitor.Enter函数时,调用Monitor.Exit 会抛出异常)

        然而这段代码存在一个微妙的漏洞。试想这样一种(不太可能的)情况:当Monitor.Enter方法内部抛出异常,或是在调用Monitor.Enter之后、进入try代码块之前发生异常(例如线程被强制中止Abort,或是抛出内存耗尽异常OutOfMemoryException)。此时锁可能被获取,也可能未被获取。如果锁已被获取,它将永远不会被释放——因为我们无法进入try/finally代码块,最终导致锁泄漏。 为避免这种风险,CLR 4.0的设计者为Monitor.Enter添加了以下重载方法:

public static void Enter (object obj, ref bool lockTaken);

当(且仅当)Enter方法抛出异常且未成功获取锁时,该方法执行后lockTaken参数值为false。以下是正确的使用模式(这也正是C# 4.0编译lock语句时生成的代码逻辑):   

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

这样一来,即使在Enter函数出现异常,也不会去执行Monitor.Exit函数了。

Monitor 类还提供了 TryEnter 方法,允许指定超时时间(以毫秒或 TimeSpan 形式)。如果成功获取锁,该方法返回 true;如果因超时未能获取锁,则返回 falseTryEnter 也可以不带参数调用,此时它会立即“测试”锁的状态,如果无法立即获取锁,则立刻超时返回。

与 Enter 方法类似,在 CLR 4.0 中,TryEnter 也提供了接受 lockTaken 参数的重载版本。

这里就不过多介绍了,感兴趣的可以去看官方链接

3.锁的其它要注意的问题

3.1同步对象的选择

        任何对参与线程可见的对象都可以作为同步对象,但必须遵循一个硬性规则:同步对象必须是引用类型。同步对象通常声明为 private(这有助于封装锁逻辑),并且通常是实例字段或静态字段。同步对象可以同时作为被保护对象本身,如下例中的 _list 字段所示:

class ThreadSafe
{List 
<string> _list = new List <string>();void Test(){lock (_list){_list
.Add ("Item 1");...

专门用于加锁的字段(如前例中的 _locker)能够精确控制锁的作用范围和粒度。此外,包含对象本身(this)或其类型(typeof(ClassName))也可作为同步对象使用:

lock (this) { ... }
lock (typeof (Widget)) { ... }    // For protecting access to statics

虽然上面两个锁对象都是合理的,却是不建议的:

使用this作为锁对象会造成:

  • 外部代码可能锁定你的对象实例,导致死锁。
  • 破坏了面向对象的封装原则。

使用类类型作为锁对象则更糟糕:typeof(ClassName) 返回的是类的 Type 对象,该对象在 AppDomain 范围内是唯一的。所有线程中任何使用 lock(typeof(ClassName)) 的代码都会竞争同一个锁,导致: ◦  性能瓶颈:无关代码因共享同一个锁而阻塞。  ◦  死锁风险:第三方库或框架若恰好也锁定了该类型,可能引发不可预料的死锁

3.2什么时候该上锁

        首先,如果你确定你的程序是单线程的,那任何时候都不需要上锁。否则,上锁基本原则是:任何对可写共享字段的访问都需要加锁即使是最简单的单字段赋值操作,也必须考虑同步问题。例如以下类中,无论是Increment还是Assign方法都不是线程安全的:

class ThreadUnsafe
{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; }
}

3.3锁和原子性

        如果一组变量总是在同一个锁内进行读写,那么可以认为这些变量的读写操作是原子性的。假设字段 x 和 y 始终在对 locker 对象加锁的情况下进行读写:

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

那么我们可以说 x 和 y 的访问是原子性的,因为这段代码块不会被其他线程的操作分割或抢占,从而避免 x 或 y 被意外修改而导致结果失效。只要 x 和 y 始终在同一个独占锁内访问,就永远不会发生除零错误。

3.4嵌套锁

        一个线程可以反复的对一个对象添加锁:

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

或者改用Monitor类:

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 Main()
{lock (_locker){AnotherMethod();// We still have the lock - because locks are reentrant.}
}static void AnotherMethod()
{lock (_locker) { Console.WriteLine ("Another method"); }
}

3.5 死锁

        死锁在多线程编程是比较常见的。下面这个代码就会触发死锁:

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
}

        在多线程编程中,死锁是最棘手的难题之一——尤其是当存在大量相互关联的对象时。究其根本,难点在于你永远无法确定调用方已经获取了哪些锁。 设想这样一个场景:你可能在类X中无意识地锁定了私有字段a,却不知道调用方(或调用方的调用方)已经在类Y中锁定了字段b。与此同时,另一个线程正以相反的顺序执行锁定——这就形成了死锁。

        颇具讽刺意味的是,这种问题反而会因(良好的)面向对象设计模式而加剧,因为这些模式创建的调用链直到运行时才能确定。 虽然"按固定顺序锁定对象以避免死锁"的建议在我们最初的示例中很有帮助,但很难适用于上述场景。

        更明智的策略是:当持有锁的情况下调用可能反向引用自身对象的方法时要格外谨慎。同时,需要审慎评估是否真的有必要在调用其他类的方法时保持锁定(虽然很多时候确实需要——我们稍后会讨论——但有时存在其他选择)。更多地依赖声明式编程、数据并行、不可变类型以及非阻塞同步结构,可以减少对锁定的依赖。 

        这个问题还可以换个角度理解:当持有锁时调用外部代码,锁的封装性就会在无形中被破坏。这不是CLR或.NET框架的缺陷,而是锁机制与生俱来的局限性。目前包括软件事务内存(Software Transactional Memory)在内的多个研究项目正在尝试解决锁机制带来的各种问题。

         另一个典型的死锁场景发生在WPF应用程序调用Dispatcher.Invoke或Windows Forms应用程序调用Control.Invoke时——如果此时恰好持有锁,而UI线程正在执行另一个等待同一锁的方法,就会立即引发死锁。通常只需改用BeginInvoke而非Invoke即可解决。当然,也可以在调用Invoke前释放锁,不过如果锁是由调用方获取的,这个方法就不适用了。我们将在"富客户端应用与线程关联性"章节详细解释Invoke和BeginInvoke的机制。

3.6 性能

        加锁操作本身非常高效:在2010年代的计算机上,如果锁未被争用,获取和释放一个锁最快仅需20纳秒。但当锁出现争用时,随之而来的上下文切换会使开销激增至微秒级别——如果线程需要重新调度,等待时间可能更长。对于极短时间的锁定,使用SpinLock类可以避免上下文切换的开销。 需要注意的是,如果锁持有时间过长,不仅会降低并发性能,还会显著增加死锁风险。锁的争用会引发线程阻塞,当多个线程相互等待对方释放锁时,系统吞吐量将急剧下降。因此,开发者需要在保证线程安全的前提下,尽量缩小临界区范围,并考虑使用读写锁(ReaderWriterLockSlim)等更细粒度的同步机制来提升并发性。对于高并发场景,无锁编程(lock-free programming)或不可变数据结构往往是更好的选择。

4.Mutex

        互斥锁(Mutex)类似于 C# 的 lock 语句,但它的作用范围可以跨越多个进程。也就是说,Mutex 既可以是应用程序级别的,也可以是计算机全局范围的。( 获取和释放一个无竞争的 Mutex 需要几微秒时间——这比 lock 语句慢了约 50 倍。)

        使用 Mutex 类时,你需要调用 WaitOne 方法来加锁,调用 ReleaseMutex 方法来解锁。关闭或释放 Mutex 会自动解除锁定。与 lock 语句一样,Mutex 只能由获取它的同一个线程来释放。 跨进程 Mutex 的一个常见用途是确保同一时间只能运行一个程序实例。具体实现如下:

class OneAtATimePlease
{static void Main(){// Naming a Mutex makes it available computer-wide. Use a name that's// unique to your company and application (e.g., include your URL).using (var mutex = new Mutex (false, "oreilly.com OneAtATimeDemo")){// Wait a few seconds if contended, in case another instance// of the program is still in the process of shutting down.if (!mutex.WaitOne (TimeSpan.FromSeconds (3), false)){Console.WriteLine ("Another app instance is running. Bye!");return;}RunProgram();}}static void RunProgram(){Console.WriteLine ("Running. Press Enter to exit");Console.ReadLine();}
}

当然也可以这样实现:

static void Main()
{using var mutex = new Mutex(true, "Global\\MyApp", out bool createdNew);if (!createdNew){Console.WriteLine("程序已在运行中!");return;}// 主程序逻辑Console.WriteLine("程序启动...");Console.ReadLine();
}

一般而言,mutex在多线程编程中使用的不多,lock是更常见的选择。但涉及到跨进程时,lock可能就无能为力了,这是可以考虑mutex.

5.Semaphore

        信号量(Semaphore)就像一家夜总会:它有一定的容量限制,由门口的保安严格执行。一旦满员,其他人就无法进入,只能在门外排队等候。每当有一个人离开,队首的一个人就能进入。它的构造函数至少需要两个参数:当前夜总会内的空位数,以及夜总会的总容量

容量为1的信号量与互斥锁(Mutex)或lock类似,但关键区别在于信号量没有"所有者"——它对线程是透明的。任何线程都可以调用信号量的Release方法,而Mutex和lock只能由获取锁的线程来释放

        (这个类有两个功能相似的版本:Semaphore和SemaphoreSlim。后者是在.NET Framework 4.0中引入的,针对并行编程的低延迟需求进行了优化。它在传统多线程编程中也很有用,因为它允许在等待时指定取消令牌。不过,它不能用于进程间通信。 Semaphore执行WaitOne或Release大约需要1微秒;而SemaphoreSlim只需要前者的四分之一时间。 )

信号量在限制并发度方面非常有用——可以防止过多线程同时执行某段代码。在下面的例子中,五个线程试图进入一家同时只允许三个线程进入的"夜总会":

        

class TheClub      // No door lists!
{static SemaphoreSlim _sem = new SemaphoreSlim (3);    // Capacity of 3static void Main(){for (int i = 1; i <= 5; i++) new Thread (Enter).Start (i);}static void Enter (object id){Console.WriteLine (id + " wants to enter");_sem.Wait();Console.WriteLine (id + " is in!");           // Only three threadsThread.Sleep (1000 * (int) id);               // can be here atConsole.WriteLine (id + " is leaving");       // a time._sem.Release();}
}

执行结果如下:

1 wants to enter
1 is in!
2 wants to enter
2 is in!
3 wants to enter
3 is in!
4 wants to enter
5 wants to enter
1 is leaving
4 is in!
2 is leaving
5 is in!

如果将 Sleep 语句替换为密集的磁盘 I/O 操作,信号量(Semaphore)通过限制过多的并发硬盘访问,反而能够提升整体性能。 如果给信号量命名,它就能像互斥锁(Mutex)一样实现跨进程同步


本小节就介绍到这里,下面一节将介绍线程安全的一些实现准则。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/917264.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/917264.shtml
英文地址,请注明出处:http://en.pswp.cn/news/917264.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

鸿蒙智能居家养老系统构思(续二)—— 适老化烹饪中心详细构思

一、背景在“写给华为鸿蒙智家 —— 智能居家养老系统构思”一文中&#xff0c;结合对居家养老的理解及个人体验&#xff0c;提出了基于鸿蒙OS实现居家养老系统的粗略构思。其中包含“吃得好”。当老人到了不能随性外出活动、只能在家消耗时光时&#xff0c;除了一些看看电视、…

高斯透镜公式(调整镜头与感光元件之间的距离时,使得不同距离的物体在感光元件上形成清晰的影像)

当使用定焦镜头时&#xff0c;仍然可以调整镜头与感光元件&#xff08;或胶片&#xff09;之间的距离时&#xff0c;使得不同距离的物体在感光元件上形成清晰的影像。对此可以用高斯透镜公式进行解释&#xff1a; 一、透镜成像的基本原理 在光学中&#xff0c;一个基本的公式是…

预过滤环境光贴图制作教程:第三阶段 - GGX 分布预过滤

核心目标 GGX 分布是 PBR 中模拟粗糙表面高光反射的主流模型,其核心是通过统计分布描述微表面的朝向概率。本阶段的目标是: 基于第一阶段生成的环境图集,预计算 6 个级别的 GGX 过滤结果(对应不同粗糙度); 使用蒙特卡洛采样(Monte Carlo Sampling)加速 GGX 卷积计算;…

Spring框架与AutoCAD结合应用

什么是AutoCAD? AutoCAD简介 AutoCAD是由美国Autodesk公司开发的计算机辅助设计(CAD)软件,广泛应用于建筑、工程、制造、产品设计等领域。它支持2D绘图和3D建模,提供精确的图形工具和自动化功能,帮助用户高效创建技术图纸和模型。 主要功能 2D绘图:提供直线、圆弧、多…

Java 学习笔记:常用类、String 与日期时间处理

作为一名名 Java 初学者&#xff0c;最近在学习过程中整理了一些关于常用类、String 类以及日期时间处理的知识点。这些内容是 Java 基础中的重点&#xff0c;也是日常编程练习中频繁用到的工具&#xff0c;掌握它们能让我们在写代码时更加得心应手。今天把这些笔记分享出来&am…

Android常用的adb和logcat命令

ADB ADB&#xff0c;即 Android Debug Bridge 是一种允许模拟器或已连接的 Android 设备进行通信的命令行工具&#xff0c;它可为各种设备操作提供便利&#xff0c;如安装和调试应用&#xff0c;并提供对 Unix shell&#xff08;可用来在模拟器或连接的设备上运行各种命令&…

重学JS-001 --- JavaScript算法与数据结构(一)JavaScript 基础知识

文章目录 变量 变量命名规则 变量命名 let vs const 变量使用范围 赋值 = 控制台输出 运算符 ++ -- == === !== 注释 转义字符 数据类型 7种 原始数据类型 1. string​​ 2. number​​ 3. ​​boolean​​ 4. null​​ 5. undefined​​ 6. ​​symbol​​(ES6 新增) 7. big…

MySQL数据闪回工具my2sql的使用

场景&#xff1a; 当你或者其它人员误操作数据库不小心删除或者更新了一批数据&#xff0c;但是是当时又没事先备份时&#xff0c;你可以 用这个 my2sql工具快速帮你找回数据。就是如此的丝滑。但是要注意的是只限于dml语句&#xff0c;所以我们在操作数据库前必需先备份哦&…

9.1无法恢复的错误与 panic!

无法恢复的错误与 panic! 有时你的代码中会发生严重问题&#xff0c;而你无能为力。在这些情况下&#xff0c;Rust 提供了 panic! 宏。实际上&#xff0c;有两种方式会导致 panic&#xff1a;一种是执行某个操作使代码产生 panic&#xff08;例如访问数组越界&#xff09;&…

分享低功耗单火线开关语音识别方案

在众多老旧建筑和常规家居环境里&#xff0c;单火线布线是主流方式。单火线语音识别芯片方案通过研发和应用特殊的单火线语音识别芯片&#xff0c;实现设备在单火线供电条件下稳定运行&#xff0c;并精准识别语音指令&#xff0c;为智能家居、智能照明等领域带来便捷的语音控制…

如何在Windows操作系统上通过conda 安装 MDAnalysis

MDAnalysis 是一个开源的 Python 库,旨在提供一个高效且灵活的方式来分析和处理分子动力学(MD)模拟数据。它可以从不同的文件格式中读取模拟轨迹和结构数据,进行复杂的数据处理和分析,广泛应用于生物物理学、化学、材料科学等领域。 一、创建虚拟环境 为了能够顺利安装,减…

实用PDF演示解决方案

它打破了传统阅 读模式&#xff0c;让PDF文档也能像PPT一样流畅播放&#xff0c;特别适合汇报、讲解等展示场景。它是绿色单文件版&#xff0c;无需安装&#xff0c;双击红色图标即点即用。运行后第一件事&#xff0c;建议把界面语言切换成中文&#xff0c;操作更顺手。导入PDF…

VS Code中如何关闭Github Copilot

点击顶部搜索栏后面的Copilot图标&#xff0c;在下拉菜单中选择Hide Copilot。在弹出的提示框中&#xff0c;点击Hide Copilot按钮就可以了。

MySQL学习从零开始--第六部分

Binlog是什么&#xff1f;有哪几种格式&#xff1f;推荐使用哪种&#xff0c;为什么 Binlog是什么 Binlog二进制日志是MySQL Server层记录所有更改数据库内容的操作日志的二进制文件&#xff0c;如操作UPDATE,DELETE,INSERTBinlog不记录SELECT&#xff0c;SHOW等查询操作使主从…

走进computed,了解computed的前世今生

computed&#xff08;计算属性&#xff09;并不是vue独创的&#xff0c;而是源自计算机科学和响应式编程的长期发展 计算理论的奠基&#xff1a; 函数式编程的纯函数思想&#xff1a;计算属性的核心特征&#xff08;无副作用、依赖输入确定输出&#xff09;直接来源于函数式编程…

Java 23 新特性解析与代码示例

Java 23 新特性解析与代码示例 文章目录Java 23 新特性解析与代码示例1. 引言2. 正式特性2.1. Markdown文档注释 (JEP 467)2.2. 废弃sun.misc.Unsafe的内存访问方法以移除 (JEP 471)2.3. ZGC&#xff1a;默认启用代际模式 (JEP 474)3. 预览特性3.1. 原始类型在模式、instanceof…

spring boot + mybatis + mysql 只有一个实体类的demo

使用MyBatis进行数据库操作&#xff0c;配置简单。主要演示了mybatis可以不用只使用方法名来对应mapper.java和mapper.xml。 目录结构 pom.xml src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── springbootjdbcweb/ │ │ └── …

iRemovalPro完美绕iCloud插卡打电话,A12+支持iOS 18.1.1

iRemovalPro 专业工具全解析与操作指南 &#xff08;支持iOS 14.0 - 16.6.1&#xff0c;A7-A15芯片设备&#xff09; &#x1f449;下载地址见文末 iRemoval Pro iRemoval 专业版是一款来自外国安全研究员的工具&#xff0c;用来帮助一些人因为忘记自己的ID或者密码&#xff0c…

安卓SELinux策略语法

目录前言一、 通用AV规则语法1.1 allow source target:class permissions;1.2 neverallow source target:class permissions;二、type三、attribute四、typeattribute五、alias六、typealias七、init_daemon_domain7.1 init_daemon_domain 宏概述7.2 宏展开与实现7.2.1 展开后规…

vscode cursor配置php的debug,docker里面debug

VSCode PHP调试配置指南 概述 本文介绍如何在VSCode中配置PHP调试环境&#xff0c;包括本地和Docker环境。 前置要求 VSCodePHP 7.0Xdebug扩展PHP Debug VSCode扩展 本地调试配置 1. 安装Xdebug # Ubuntu/Debian sudo apt-get install php-xdebug# MacOS brew install p…