编程博客
002、修改Nuget包位置
001、C#常用的单词
003、收藏的书签
回家准备
004、ASP.NET Core 3.0 gRPC
001、ASP.NET Core 3.0 使用gRPC
002、ASP.NET Core 3.0 gRPC 双向流
003、ASP.NET Core 3.0 gRPC 身份认证和授权
005、飞牛NAS
001、 FnOS飞牛系统实用配置1-应用远程访问(迅雷)
006、DeepSeek
001、DeepSeek 使用指南
007、并发编程
01、并发编程 - 死锁的产生、排查与解决方案
02、并发编程 - 初识线程
03、并发编程 - 线程浅试
04、并发编程 - 线程同步(一)
05、并发编程 - 线程同步(二)
06、并发编程 - 线程同步(三)之原子操作 Interlocked 简介
07、并发编程 - 线程同步(四)之原子操作 Interlocked 详解一
08、并发编程 - 线程同步(五)之原子操作 Interlocked 详解二
09、并发编程 - 线程同步(六)之锁 lock
008、启动项
03、应用--Program 中的 WebApplication
02、主机--Host
01、Program 文件的作用
04、控制反转 IOC 与依赖注入 DI
05、中间件
06、Logger 原理及配置 Log4Net
07、ElasticSearch
本文档使用 MrDoc 发布
-
+
首页
09、并发编程 - 线程同步(六)之锁 lock
通过前面对 Interlocked 类的学习,相信大家对线程同步机制有了更深的理解,今天我们将继续需要另一种同步机制——锁 lock。  lock 是 C#语言中的关键字,是线程同步机制的一种简单的互斥锁实现方式,它可以保证在同一时刻只有一个线程能够访问被锁定的代码块。其工作原理也很简单,就是通过 lock 创建一个互斥锁,当一个线程获取到此互斥锁则此线程可以进入被 lock 保护的代码块,同时其他线程将被阻塞无法进入此代码块,直至第一个线程释放此互斥锁,其他线程才可以获取此互斥锁并进入代码块。  lock 的使用也非常简单,语法如下: ```csharp lock (obj) { //线程不安全的代码块 } ``` 虽然 lock 使用起来简单方便,但是使用方式不正确也很容易产生各种奇奇怪怪的问题。 # ***01***、避免锁定 this 这种使用方式会导致两个问题: 1.不可控性:lock(this)锁定的范围是整个实例,这也就意味着其他线程可以通过该实例中的其他方法访问该锁,进而形成一个实例中多个使用 lock(this)的方法之前相互影响。 2.外部可见性:this 表示当前实例的引用,它是公共的,因此外部代码也可以访问,这也就意味着外部代码可以通过 lock(实例)访问 lock(this)锁,从而使同步机制失去控制。 下面我们直接看代码: ```csharp public class LockThisExample { public void Method1() { lock (this) { var threadId = Thread.CurrentThread.ManagedThreadId; Console.WriteLine($"线程 {threadId} 通过lock(this)锁进入 Method1"); Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}"); Console.WriteLine($"开始休眠 5 秒"); Console.WriteLine($"------------------------------------"); Thread.Sleep(5000); } } public void Method2() { lock (this) { var threadId = Thread.CurrentThread.ManagedThreadId; Console.WriteLine($"线程 {threadId} 通过lock(this)锁进入 Method2"); Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}"); } } } public static void LockThisRun() { var example = new LockThisExample(); var thread1 = new Thread(example.Method1); var thread2 = new Thread(example.Method2); var thread3 = new Thread(() => { lock (example) { var threadId = Thread.CurrentThread.ManagedThreadId; Console.WriteLine($"线程 {threadId} 通过lock(实例)锁进入 Method3"); Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}"); Console.WriteLine($"开始休眠 5 秒"); Console.WriteLine($"------------------------------------"); Thread.Sleep(5000); } }); thread3.Start(); thread1.Start(); thread2.Start(); } ``` 我们看看代码执行结果:  这里例子可以很好的说明 lock(this)代理的问题,原本可以三个线程并发执行的三段代码,因为使用了同一个锁,导致三个线程只能顺序执行。其中 Method1 和 Method2 体现了同一实例内方法相互影响,Method3 和 Method1、Method2 体现了因为相同实例导致实例内部方法和实例外部方法相互影响。 # ***02***、避免锁定公共对象 这种使用方式会导致两个问题: 1.全局影响:公共对象,特别是 public static 对象,很大概率会被多个类,甚至多个模块引用,因此锁定公共对象很可能导致全局范围内的同步,大大增加了死锁、竞争条件的产生的风险。 2.不可预测性:因为公共对象对全局可访问,因此如果其他模块锁定此公共对象,则当出现问题时将难以排除调试问题。 看下面代码: ```csharp public class PublicLock { public static readonly object Lock = new object(); } public class LockPublic1Example { public void Method1() { lock (PublicLock.Lock) { var threadId = Thread.CurrentThread.ManagedThreadId; Console.WriteLine($"线程 {threadId} 通过 lock(公共对象) 锁进入 Public1"); Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}"); Console.WriteLine($"开始休眠 5 秒"); Console.WriteLine($"------------------------------------"); Thread.Sleep(5000); } } } public class LockPublic2Example { public void Method1() { lock (PublicLock.Lock) { var threadId = Thread.CurrentThread.ManagedThreadId; Console.WriteLine($"线程 {threadId} 通过 lock(公共对象) 锁进入 Public2"); Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}"); } } } public static void LockPublicRun() { var example1 = new LockPublic1Example(); var example2 = new LockPublic2Example(); var thread1 = new Thread(example1.Method1); var thread2 = new Thread(example2.Method1); thread1.Start(); thread2.Start(); } ``` 在看看执行结果:  可以发现因为锁定了同一个公共对象,导致两个不同线程的不同实例,还是产生互相争抢锁的问题。 # ***03***、避免锁定字符串 在 C#中,字符串因其不可变性和字符串池的原因,在整个程序中一个字符串一旦创建就不会更改,如果对其修改则产生新的字符串对象,而原字符串对象保持不变;同时如果创建两个相同内容的字符串,则它们共享同一个内存地址。 这就导致锁定字符串极其危险尤其危险,因为整个程序中任何给定字符串都只有一个实例,而在整个程序中只有锁定相同内容的字符串都会形成竞争条件。 ```csharp public class LockString1Example { public void Method1() { lock ("abc") { var threadId = Thread.CurrentThread.ManagedThreadId; Console.WriteLine($"线程 {threadId} 通过 lock(字符串) 锁进入 String1"); Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}"); Console.WriteLine($"开始休眠 5 秒"); Console.WriteLine($"------------------------------------"); Thread.Sleep(5000); } } } public class LockString2Example { public void Method1() { lock ("abc") { var threadId = Thread.CurrentThread.ManagedThreadId; Console.WriteLine($"线程 {threadId} 通过 lock(字符串) 锁进入 String2"); Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}"); } } } public static void LockStringRun() { var example1 = new LockString1Example(); var example2 = new LockString2Example(); var thread1 = new Thread(example1.Method1); var thread2 = new Thread(example2.Method1); thread1.Start(); thread2.Start(); } ``` 我们看看执行结果:  可以发现虽然在两个类中分别使用了两个字符串“abc”,但对于整个程序来说它们都指向了同一个实例,因此共用了一把锁。 # ***04***、小心锁定非 readonly 对象 这是因为如果锁对象为非只读对象,就可能发生某个 lock 代码块中修改锁对象,从而导致 锁对象变更,进而使得其他线程可以畅通无阻的进入该代码块。 如下示例: ```csharp public class LockNotReadonlyExample { private object _lock = new object(); public void Method1() { lock (_lock) { _lock = new object(); var threadId = Thread.CurrentThread.ManagedThreadId; Console.WriteLine($"线程 {threadId} 进入 Method1 , 时间 {DateTime.Now:HH:mm:ss}"); Console.WriteLine($"------------------------------------"); Thread.Sleep(5000); } } } public static void LockNotReadonlyRun() { var example = new LockNotReadonlyExample(); var thread1 = new Thread(example.Method1); var thread2 = new Thread(example.Method1); var thread3 = new Thread(example.Method1); thread1.Start(); thread2.Start(); thread3.Start(); } ``` 再来看执行结果:  可以发现三个线程几乎同时进入,lock 根本就没有起到锁的作用。 # ***05***、小心锁定静态对象 对于是否需要锁定静态对象取决于你的需求。 1.如果要在静态方法中使用 lock 时,则锁定的对象也必须要是静态对象。 2.如果希望类的每个实例都有独立的锁对象,则锁定非静态对象。 3.如果希望类的所有实例共享同一个锁,则锁定静态对象。 代码示例如下: ```csharp public class LockStaticExample { //这是一个实例字段,意味着类的每个实例都会有一个独立的锁对象。 //如果你希望类的每个实例有自己独立的锁来控制并发访问,这种方式更合适。 private readonly object _lock1 = new object(); //这是一个静态字段,意味着类的所有实例共享同一个锁对象。 //如果你希望类的所有实例都共享同一个锁来同步对某个静态资源访问,这种方式更合适。 private static readonly object _lock2 = new object(); public void Method1() { lock (_lock1) { // 临界区代码 } } public void Method2() { lock (_lock2) { // 临界区代码 } } public static void Method3() { lock (_lock2) { // 临界区代码 } } } ``` 这是因为静态字段是所有实例共享的,其内存地址在整个程序的生命周期内是唯一的,所有实例访问同一个内存地址,因此锁定静态对象时要特别小心。
个人天使
2025年2月10日 10:30
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码