C# lock使用详解

总目录


前言

在 C# 多线程编程中,lock 关键字是一种非常重要的同步机制,用于确保同一时间只有一个线程可以访问特定的代码块,从而避免多个线程同时操作共享资源时可能出现的数据竞争和不一致问题。以下是关于 lock 关键字的详细使用介绍。


一、基本概念

lock 关键字提供了一种简单的方法来实现互斥锁(mutex),它通过锁定一个对象来保护临界区内的代码。当一个线程进入被 lock 保护的代码块时,其他尝试进入同一代码块的线程将被阻塞,直到第一个线程完成并释放锁。

lock 关键字的基本语法如下:

lock (object)
{// 要保护的代码块/临界区代码
}

其中,object 是一个引用类型的对象,通常是一个私有的静态或实例成员变量,通常称为 “锁对象”。lock 语句会获取这个锁对象的独占锁,当一个线程进入 lock 块时,它会尝试获取锁对象的锁。如果锁对象当前没有被其他线程持有,该线程会获取锁并执行 lock 块内的代码;如果锁对象已经被其他线程持有,当前线程会被阻塞,直到持有锁的线程退出 lock 块并释放锁。

为了确保线程安全,所有需要同步访问的代码都应该使用同一个锁对象。

二、使用

1. 基本使用

下面是一个简单的示例,展示了如何使用 lock 关键字来保护共享资源:

using System;
using System.Threading;class Program
{private static int sharedCounter = 0;private static readonly object lockObject = new object();static void Main(){// 创建两个线程Thread thread1 = new Thread(IncrementCounter);Thread thread2 = new Thread(IncrementCounter);// 启动线程thread1.Start();thread2.Start();// 等待两个线程执行完毕thread1.Join();thread2.Join();// 输出最终的计数器值Console.WriteLine($"Final counter value: {sharedCounter}");}static void IncrementCounter(){for (int i = 0; i < 100000; i++){// 使用 lock 关键字保护共享资源lock (lockObject){sharedCounter++;}}}
}

在这个示例中,sharedCounter 是一个共享资源,多个线程可能同时对其进行递增操作。为了避免数据竞争,我们使用 lock 关键字来保护 sharedCounter 的递增操作。lockObject 是一个用于锁定的对象,两个线程在执行 sharedCounter++ 之前都会尝试获取 lockObject 的锁,只有获取到锁的线程才能执行递增操作,从而确保同一时间只有一个线程可以修改 sharedCounter 的值。

2. 静态 vs 实例锁

根据锁对象是静态成员还是实例成员,lock 可以保护类级别的资源或对象级别的资源:

  • 静态锁:用于保护类的所有实例之间的共享资源。
  • 实例锁:用于保护单个对象的状态。

示例:静态锁 vs 实例锁

public class Singleton
{private static readonly object _staticLock = new object();private readonly object _instanceLock = new object();public void StaticMethod(){lock (_staticLock){// Shared resource access}}public void InstanceMethod(){lock (_instanceLock){// Object-specific resource access}}
}

三、 注意事项

1. 锁的粒度

合理控制锁的粒度对于性能至关重要:

  • 粗粒度锁:锁定整个方法或较大的代码段。虽然简单易用,但可能导致不必要的阻塞,影响吞吐量。
  • 细粒度锁:只锁定必要的最小代码片段。这样可以减少等待时间,提高并发性,但也增加了复杂性和潜在的死锁风险。

最佳实践

  • 要合理控制锁的粒度,即 lock 块内的代码量。如果锁的粒度过大,会导致其他线程等待的时间过长,降低程序的性能;如果锁的粒度过小,可能无法有效保护共享资源,仍然会出现数据竞争问题。
  • 尽量缩小锁的作用范围,只锁定那些真正需要同步的代码行。此外,避免在锁内部执行长时间运行的操作,如I/O访问或复杂的计算。

2. 避免死锁

死锁是指两个或多个线程相互等待对方释放锁,从而导致所有线程都无法继续执行的情况。为了避免死锁,应该尽量减少锁的嵌套,确保线程按照相同的顺序获取锁。

关于死锁详情,下文会有详细介绍。

3. 锁对象的选择

  • 专用锁对象:确保所有需要同步的代码使用相同的锁对象。通常建议使用一个专用的、私有的引用类型对象作为锁对象,如上面示例中的 lockObject。这样可以避免不同的代码块或类之间意外地共享同一个锁对象,从而减少死锁和其他同步问题的发生。
  • 避免使用 this:在实例方法中,不建议使用 this 作为锁对象,因为其他代码可能会无意中锁定同一个对象,导致意想不到的结果。例如:
class MyClass
{private int counter = 0;public void Increment(){// 不推荐使用 this 作为锁对象lock (this){counter++;}}
}
  • 避免使用字符串:也不建议使用字符串作为锁对象,因为字符串具有字符串驻留机制,可能会导致不同的代码块使用相同的字符串作为锁对象,从而引发同步问题。

四、死锁

死锁指的是两个或多个线程相互等待对方释放锁,从而导致所有线程都无法继续执行的情况

1. 死锁示例

以下是一个用 C# 编写的会发生死锁的案例,这个案例模拟了两个线程互相等待对方释放锁的情况,从而导致死锁。

using System;
using System.Threading;class DeadlockExample
{// 定义两个锁对象private static readonly object lock1 = new object();private static readonly object lock2 = new object();static void Main(){// 创建第一个线程Thread thread1 = new Thread(Function1);// 创建第二个线程Thread thread2 = new Thread(Function2);// 启动第一个线程thread1.Start();// 稍微延迟一下,让 thread1 有机会先获取 lock1Thread.Sleep(100);// 启动第二个线程thread2.Start();// 等待两个线程执行完成thread1.Join();thread2.Join();Console.WriteLine("程序执行结束");}static void Function1(){// 线程 1 先获取 lock1lock (lock1){Console.WriteLine("线程 1 已获取 lock1,正在尝试获取 lock2...");// 稍微延迟一下,增加死锁发生的概率Thread.Sleep(200);// 线程 1 尝试获取 lock2lock (lock2){Console.WriteLine("线程 1 已获取 lock2");}}}static void Function2(){// 线程 2 先获取 lock2lock (lock2){Console.WriteLine("线程 2 已获取 lock2,正在尝试获取 lock1...");// 稍微延迟一下,增加死锁发生的概率Thread.Sleep(200);// 线程 2 尝试获取 lock1lock (lock1){Console.WriteLine("线程 2 已获取 lock1");}}}
}

代码解释

  1. 锁对象的定义:
  • lock1 和 lock2 是两个静态的、只读的 object 类型对象,作为锁使用。
  1. 线程的创建和启动:
  • thread1 执行 Function1 方法,thread2 执行 Function2 方法。
  • 在启动 thread2 之前,让主线程休眠 100 毫秒,目的是让 thread1 有机会先获取 lock1。
  1. Function1 方法:
  • 线程 1 首先获取 lock1,然后输出提示信息,表示正在尝试获取 lock2。
  • 线程 1 休眠 200 毫秒,增加死锁发生的概率。
  • 线程 1 尝试获取 lock2。
  1. Function2 方法:
  • 线程 2 首先获取 lock2,然后输出提示信息,表示正在尝试获取 lock1。
  • 线程 2 休眠 200 毫秒,增加死锁发生的概率。
  • 线程 2 尝试获取 lock1。

死锁的产生过程

  • 线程 1 先获取了 lock1,然后尝试获取 lock2。
  • 线程 2 先获取了 lock2,然后尝试获取 lock1。
  • 此时,线程 1 持有 lock1 并等待 lock2,而线程 2 持有 lock2 并等待 lock1,两个线程互相等待对方释放锁,从而导致死锁,程序将无法继续执行下去。

运行这个程序时,你会看到控制台输出类似以下的内容:

线程 1 已获取 lock1,正在尝试获取 lock2...
线程 2 已获取 lock2,正在尝试获取 lock1...

之后程序就会停滞,因为发生了死锁。

2. 如何避免死锁

在多线程编程里,死锁是一个常见且棘手的问题,它指的是两个或多个线程相互等待对方释放锁,从而导致所有线程都无法继续执行的情况。以下是一些避免死锁问题的有效方法:

1) 按顺序获取锁

多个线程在需要获取多个锁时,如果都按照相同的顺序获取锁,就能避免死锁。例如,假设有两个锁 lockA 和 lockB,所有线程都先获取 lockA 再获取 lockB,这样就不会出现一个线程持有 lockA 等待 lockB,而另一个线程持有 lockB 等待 lockA 的情况。

using System;
using System.Threading;class Program
{private static readonly object lockA = new object();private static readonly object lockB = new object();static void Main(){Thread thread1 = new Thread(() =>{lock (lockA){Console.WriteLine("Thread 1 acquired lockA");Thread.Sleep(100);lock (lockB){Console.WriteLine("Thread 1 acquired lockB");}}});Thread thread2 = new Thread(() =>{lock (lockA){Console.WriteLine("Thread 2 acquired lockA");Thread.Sleep(100);lock (lockB){Console.WriteLine("Thread 2 acquired lockB");}}});thread1.Start();thread2.Start();thread1.Join();thread2.Join();}
}

2) 设置锁的超时时间

为锁操作设置超时时间,当线程在规定时间内无法获取锁时,就放弃获取锁并进行其他处理。这样可以避免线程无限期地等待锁,从而打破死锁的条件。在 C# 中,可以使用 Monitor.TryEnter 方法来实现这一点。

using System;
using System.Threading;class Program
{private static readonly object lockA = new object();private static readonly object lockB = new object();static void Main(){Thread thread1 = new Thread(() =>{if (Monitor.TryEnter(lockA, 1000)){try{Console.WriteLine("Thread 1 acquired lockA");if (Monitor.TryEnter(lockB, 1000)){try{Console.WriteLine("Thread 1 acquired lockB");}finally{Monitor.Exit(lockB);}}}finally{Monitor.Exit(lockA);}}});Thread thread2 = new Thread(() =>{if (Monitor.TryEnter(lockA, 1000)){try{Console.WriteLine("Thread 2 acquired lockA");if (Monitor.TryEnter(lockB, 1000)){try{Console.WriteLine("Thread 2 acquired lockB");}finally{Monitor.Exit(lockB);}}}finally{Monitor.Exit(lockA);}}});thread1.Start();thread2.Start();thread1.Join();thread2.Join();}
}

3) 减少锁的嵌套

锁的嵌套会增加死锁的风险,因为嵌套的锁会使线程持有多个锁的时间变长,增加了与其他线程发生死锁的可能性。尽量减少锁的嵌套,将需要锁保护的代码逻辑拆分成更小的部分,只在必要时使用锁。

4) 使用资源层次结构

为共享资源定义一个层次结构,线程只能按照资源的层次顺序获取锁。例如,将资源分为不同的级别,线程必须先获取高级别的资源锁,再获取低级别的资源锁。这样可以确保线程获取锁的顺序是一致的,避免死锁。

5) 使用无锁算法和数据结构

在某些情况下,可以使用无锁算法和数据结构来替代传统的锁机制。无锁算法和数据结构通过原子操作(如 Interlocked 类提供的方法)来实现线程安全,避免了锁的使用,从而从根本上消除了死锁的可能性。例如,使用 ConcurrentQueue、ConcurrentDictionary<TKey, TValue> 等并发集合类。

6) 死锁检测和恢复机制

在程序中实现死锁检测机制,定期检查是否存在死锁情况。如果检测到死锁,可以采取一些恢复措施,如终止某些线程或释放某些锁,以打破死锁状态。不过,死锁检测和恢复机制的实现比较复杂,需要根据具体的应用场景进行设计。

五、lock 的实现原理

lock 关键字实际上是 Monitor 类的语法糖,上述 lock 语句等价于以下代码:

object obj = lockObject;
bool lockTaken = false;
try
{Monitor.Enter(obj, ref lockTaken);// 要保护的代码块sharedCounter++;
}
finally
{if (lockTaken){Monitor.Exit(obj);}
}

Monitor.Enter 方法用于获取锁对象的锁,Monitor.Exit 方法用于释放锁对象的锁。try-finally 块确保无论 lock 块内的代码是否抛出异常,锁都会被正确释放。

六、适用场景

lock 关键字适用于需要保护共享资源的多线程场景,例如:

  • 对共享变量的读写操作,如上面示例中的计数器。
  • 对共享集合的操作,如对 List、Dictionary<TKey, TValue> 等集合的添加、删除、修改操作。
  • 对共享文件、数据库连接等资源的访问。
  • 对于高并发场景,考虑使用 ReaderWriterLockSlim 等更灵活的同步机制。

七、其他

在异步编程 async 和 await 中,传统的 lock 并不适合,因为它会导致线程阻塞。取而代之的是,你应该考虑使用 SemaphoreSlim 或 AsyncLock 等替代方案。

using System.Threading;
using System.Threading.Tasks;public class AsyncCounter
{private int _count = 0;private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);public async Task IncrementAsync(){await _semaphore.WaitAsync();try{_count++;}finally{_semaphore.Release();}}
}

总之,lock 关键字是 C# 中一种简单而有效的同步机制,能够帮助开发者确保多线程环境下共享资源的安全访问。但在使用时需要注意锁对象的选择、锁的粒度和避免死锁等问题。


结语

回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。


参考资料:
lock 语句 - 确保对共享资源的独占访问权限
Locker 和 Monitor 锁
C#多线程系列(2):多线程锁lock和Monitor

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

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

相关文章

AAAI2024论文合集解读|Multi-granularity Causal Structure Learning-water-merged

论文标题 Multi-granularity Causal Structure Learning 多粒度因果结构学习 论文链接 Multi-granularity Causal Structure Learning 论文下载 论文作者 Jiaxuan Liang, Jun Wang, Guoxian Yu, Shuyin Xia, Guoyin Wang 内容简介 本文提出了一种新颖的方法&#xff0c;…

python3+TensorFlow 2.x(二) 回归模型

目录 回归算法 1、线性回归 (Linear Regression) 一元线性回归举例 2、非线性回归 3、回归分类 回归算法 回归算法用于预测连续的数值输出。回归分析的目标是建立一个模型&#xff0c;以便根据输入特征预测目标变量&#xff0c;在使用 TensorFlow 2.x 实现线性回归模型时&…

【景区导游——LCA】

题目 代码 #include <bits/stdc.h> using namespace std; using ll long long; const int N 1e5 10; const int M 2 * N; int p[N][18], d[N], a[N]; ll dis[N][18]; //注意这里要开long long int h[N], e[M], ne[M], idx, w[M]; int n, k; void add(int a, int b, …

家政预约小程序11分类展示

目录 1 创建页面2 配置导航菜单3 配置侧边栏选项卡4 配置数据列表5 首页和分类页联动总结 我们现在在首页开发了列表显示服务信息的功能&#xff0c;在点击导航菜单的时候&#xff0c;需要自动跳转到对应的分类&#xff0c;本篇我们介绍一下使用侧边栏选项卡实现分类显示的功能…

CVE-2023-38831 漏洞复现:win10 压缩包挂马攻击剖析

目录 前言 漏洞介绍 漏洞原理 产生条件 影响范围 防御措施 复现步骤 环境准备 具体操作 前言 在网络安全这片没有硝烟的战场上&#xff0c;新型漏洞如同隐匿的暗箭&#xff0c;时刻威胁着我们的数字生活。其中&#xff0c;CVE - 2023 - 38831 这个关联 Win10 压缩包挂…

WPF进阶 | WPF 数据绑定进阶:绑定模式、转换器与验证

WPF进阶 | WPF 数据绑定进阶&#xff1a;绑定模式、转换器与验证 一、前言二、WPF 数据绑定基础回顾2.1 数据绑定的基本概念2.2 数据绑定的基本语法 三、绑定模式3.1 单向绑定&#xff08;One - Way Binding&#xff09;3.2 双向绑定&#xff08;Two - Way Binding&#xff09;…

Java Swing 基础组件详解 [论文投稿-第四届智能系统、通信与计算机网络]

大会官网&#xff1a;www.icisccn.net Java Swing 是一个功能强大的 GUI 工具包&#xff0c;提供了丰富的组件库用于构建跨平台的桌面应用程序。本文将详细讲解 Swing 的基础组件&#xff0c;包括其作用、使用方法以及示例代码&#xff0c;帮助你快速掌握 Swing 的核心知识。 一…

题解 信息学奥赛一本通/AcWing 1118 分成互质组 DFS C++

题目 传送门 AcWing 1118. 分成互质组 - AcWing题库https://www.acwing.com/problem/content/1120/https://www.acwing.com/problem/content/1120/https://www.acwing.com/problem/content/1120/https://www.acwing.com/problem/content/1120/https://www.acwing.com/proble…

论文阅读笔记:VMamba: Visual State Space Model

论文阅读笔记&#xff1a;VMamba: Visual State Space Model 1 背景2 创新点3 方法4 模块4.1 2D选择性扫描模块&#xff08;SS2D&#xff09;4.2 加速VMamba 5 效果5.1 和SOTA方法对比5.2 SS2D和自注意力5.3 有效感受野5.4 扫描模式 论文&#xff1a;https://arxiv.org/pdf/240…

技术总结:FPGA基于GTX+RIFFA架构实现多功能SDI视频转PCIE采集卡设计方案

目录 1、前言工程概述免责声明 3、详细设计方案设计框图SDI 输入设备Gv8601a 均衡器GTX 解串与串化SMPTE SD/HD/3G SDI IP核BT1120转RGBFDMA图像缓存RIFFA用户数据控制RIFFA架构详解Xilinx 7 Series Integrated Block for PCI ExpressRIFFA驱动及其安装QT上位机HDMI输出RGB转BT…

03:Heap代码的分析

Heap代码的分析 1、内存对齐2、Heap_1.c文件代码分析3、Heap_2.c文件代码分析4、Heap_4.c文件代码分析5、Heap_5.c文件代码分析 1、内存对齐 内存对齐的作用是为了CPU更快的读取数据。对齐存储与不对齐存储的情况如下&#xff1a; 计算机读取内存中的数据时是一组一组的读取的…

自动驾驶---苏箐对智驾产品的思考

1 前言 对于更高级别的自动驾驶&#xff0c;很多人都有不同的思考&#xff0c;方案也好&#xff0c;产品也罢。最近在圈内一位知名的自动驾驶专家苏箐发表了他自己对于自动驾驶未来的思考。 苏箐是地平线的副总裁兼首席架构师&#xff0c;同时也是高阶智能驾驶解决方案SuperDri…

Android BitmapShader简洁实现马赛克/高斯模糊(毛玻璃),Kotlin(三)

Android BitmapShader简洁实现马赛克/高斯模糊&#xff08;毛玻璃&#xff09;&#xff0c;Kotlin&#xff08;三&#xff09; 发现&#xff0c;如果把&#xff08;二&#xff09; Android BitmapShader简洁实现马赛克&#xff0c;Kotlin&#xff08;二&#xff09;-CSDN博客 …

【数据结构】 并查集 + 路径压缩与按秩合并 python

目录 前言模板朴素实现路径压缩按秩合并按树高为秩按节点数为秩 总结 前言 并查集的基本实现通常使用森林来表示不同的集合&#xff0c;每个集合用一棵树表示&#xff0c;树的每个节点有一个指向其父节点的指针。 如果一个节点是它自己的父节点&#xff0c;那么它就是该集合的代…

【深度学习入门_机器学习理论】K近邻法(KNN)

本部分主要为机器学习理论入门_K近邻法(KNN)&#xff0c;书籍参考 “ 统计学习方法&#xff08;第二版&#xff09;”。 学习目标&#xff1a; 了解k近邻算法的基本概念、原理、应用&#xff1b;熟悉k近邻算法重要影响要素&#xff1b;熟悉kd树原理与优化应用。 开始本算法之…

深入理解 SQL 中的子查询

文章目录 一、什么是子查询二、子查询的基本语法三、数据准备四、子查询的分类4.1 标量子查询4.2 单行子查询4.3 多行子查询4.4 关联子查询 五、子查询的应用场景5.1 子查询与 WHERE 子句5.2 子查询与 SELECT 子句5.3 子查询与 FROM 子句 六、性能优化与注意事项 本文将深入探讨…

Zookeeper入门部署(单点与集群)

本篇文章基于docker方式部署zookeeper集群&#xff0c;请先安装docker 目录 1. docker初期准备 2.启动zookeeper 2.1 单点部署 2.2 集群部署 3. Linux脚本实现快速切换启动关闭 1. docker初期准备 拉取zookeeper镜像 docker pull zookeeper:3.5.6 如果拉取时间过长&#xf…

【SpringBoot教程】Spring Boot + MySQL + HikariCP 连接池整合教程

&#x1f64b;大家好&#xff01;我是毛毛张! &#x1f308;个人首页&#xff1a; 神马都会亿点点的毛毛张 在前面一篇文章中毛毛张介绍了SpringBoot中数据源与数据库连接池相关概念&#xff0c;今天毛毛张要分享的是关于SpringBoot整合HicariCP连接池相关知识点以及底层源码…

SCRM在企业私域流量与客户管理中的变革之路探索

内容概要 在当今数字化高速发展的时代&#xff0c;SCRM&#xff08;社交客户关系管理&#xff09;作为一种新的管理工具&#xff0c;正逐渐成为企业私域流量管理和客户关系维护的重要基石。它不仅仅是一种软件工具&#xff0c;更是一种整合客户数据和关系管理的全新思维方式。…