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");}}}
}
代码解释
- 锁对象的定义:
- lock1 和 lock2 是两个静态的、只读的 object 类型对象,作为锁使用。
- 线程的创建和启动:
- thread1 执行 Function1 方法,thread2 执行 Function2 方法。
- 在启动 thread2 之前,让主线程休眠 100 毫秒,目的是让 thread1 有机会先获取 lock1。
- Function1 方法:
- 线程 1 首先获取 lock1,然后输出提示信息,表示正在尝试获取 lock2。
- 线程 1 休眠 200 毫秒,增加死锁发生的概率。
- 线程 1 尝试获取 lock2。
- 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
相关文章:
C# lock使用详解
总目录 前言 在 C# 多线程编程中,lock 关键字是一种非常重要的同步机制,用于确保同一时间只有一个线程可以访问特定的代码块,从而避免多个线程同时操作共享资源时可能出现的数据竞争和不一致问题。以下是关于 lock 关键字的详细使用介绍。 一…...
简易CPU设计入门:控制总线的剩余信号(四)
项目代码下载 请大家首先准备好本项目所用的源代码。如果已经下载了,那就不用重复下载了。如果还没有下载,那么,请大家点击下方链接,来了解下载本项目的CPU源代码的方法。 CSDN文章:下载本项目代码 上述链接为本项目…...
使用 lock4j-redis-template-spring-boot-starter 实现 Redis 分布式锁
在分布式系统中,多个服务实例可能同时访问和修改共享资源,从而导致数据不一致的问题。为了解决这个问题,分布式锁成为了关键技术之一。本文将介绍如何使用 lock4j-redis-template-spring-boot-starter 来实现 Redis 分布式锁,从而…...
22_解析XML配置文件_List列表
解析XML文件 需要先 1.【加载XML文件】 而 【加载XML】文件有两种方式 【第一种 —— 使用Unity资源系统加载文件】 TextAsset xml Resources.Load<TextAsset>(filePath); XmlDocument doc new XmlDocument(); doc.LoadXml(xml.text); 【第二种 —— 在C#文件IO…...
编译器gcc/g++ --【Linux基础开发工具】
文章目录 一、背景知识二、gcc编译选项1、预处理(进行宏替换)2、编译(生成汇编)3、汇编(生成机器可识别代码)4、链接(生成可执行文件或库文件) 三、动态链接和静态链接四、静态库和动态库1、动静态库2、编译…...
58.界面参数传递给Command C#例子 WPF例子
界面参数的传递,界面参数是如何从前台传送到后台的。 param 参数是从界面传递到命令的。这个过程通常涉及以下几个步骤: 数据绑定:界面元素(如按钮)的 Command 属性绑定到视图模型中的 RelayCommand 实例。同时&#x…...
games101-(5/6)
光栅化 投影完成之后,视图区域被确定在从[-1,1]的单位矩阵中,下一步就是光栅化 长宽比:ratio 垂直的可视角度:fild-of-view 可以看到的y 轴的范围,角度越小 越接近正交投影 屏幕坐标系 、 将多边形转化成像素 显示…...
人工智能在计算机视觉中的应用与创新发展研究
一、引言 1.1 研究背景与意义 1.1.1 研究背景 在当今数字化与智能化飞速发展的时代,人工智能已成为推动各领域变革的核心力量,而计算机视觉作为人工智能领域中极具活力与潜力的重要分支,正发挥着日益关键的作用。计算机视觉旨在赋予计算机…...
1-2 飞机大战游戏场景
前言: 根据前面的项目框架,搭建游戏的运行场景...... 1.0 框架预览 基于该框架首先实现游戏的运行场景 2.0 图片文件 创建图片文件,本次项目使用easyx作为图形库文件,在easyx中想要显示图片,需要有一张图片和图片的掩码…...
Mac Electron 应用签名(signature)和公证(notarization)
在MacOS 10.14.5之后,如果应用没有在苹果官方平台进行公证notarization(我们可以理解为安装包需要审核,来判断是否存在病毒),那么就不能被安装。当然现在很多人的解决方案都是使用sudo spctl --master-disable,取消验证模式&#…...
Sklearn 中的逻辑回归
逻辑回归的数学模型 基本模型 逻辑回归主要用于处理二分类问题。二分类问题对于模型的输出包含 0 和 1,是一个不连续的值。分类问题的结果一般不能由线性函数求出。这里就需要一个特别的函数来求解,这里引入一个新的函数 Sigmoid 函数,也成…...
【阅读笔记】New Edge Diected Interpolation,NEDI算法,待续
一、概述 由Li等提出的新的边缘指导插值(New Edge—Di-ected Interpolation,NEDI)算法是一种具有良好边缘保持效果的新算法,它利用低分辨率图像与高分辨率图像的局部协方差问的几何对偶性来对高分辨率图像进行自适应插值。 2001年Xin Li和M.T. Orchard…...
编程题-最长的回文子串(中等)
题目: 给你一个字符串 s,找到 s 中最长的回文子串。 示例 1: 输入:s "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。示例 2: 输入:s &…...
Versal - 基础3(AXI NoC 专题+仿真+QoS)
目录 1. 简介 2. 示例 2.1 示例说明 2.2 创建项目 2.2.1 平台信息 2.2.2 AXI NoC Automation 2.2.3 创建时钟和复位 2.3 配置 NoC 2.4 配置 AXI Traffic 2.5 配置 Memory Size 2.6 Validate BD 2.7 添加观察信号 2.8 运行仿真 2.9 查看结果 2.9.1 整体波形 2.9…...
知识库建设对提升团队协作与创新能力的影响分析
内容概要 在当今快速变革的商业环境中,知识库建设的重要性愈发凸显。它不仅是信息存储的载体,更是推动组织内部沟通与协作的基石。通过系统整理与管理企业知识,团队成员能够便捷地访问相关信息,使得协作过程更为流畅,…...
Java 实现Excel转HTML、或HTML转Excel
Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,以便更好地利用和展示数据。本文将介绍如何通过 Java 实现 E…...
stack 和 queue容器的介绍和使用
1.stack的介绍 1.1stack容器的介绍 stack容器的基本特征和功能我们在数据结构篇就已经详细介绍了,还不了解的uu, 可以移步去看这篇博客哟: 数据结构-栈数据结构-队列 简单回顾一下,重要的概念其实就是后进先出,栈在…...
云计算与虚拟化技术讲解视频分享
互联网各领域资料分享专区(不定期更新): Sheet 前言 由于内容较多,且不便于排版,为避免资源失效,请用手机点击链接进行保存,若链接生效请及时反馈,谢谢~ 正文 链接如下(为避免资源失效&#x…...
python flask 使用 redis写一个例子
下面是一个使用Flask和Redis的简单例子: from flask import Flask from redis import Redisapp Flask(__name__) redis Redis(hostlocalhost, port6379)app.route(/) def hello():# 写入到Redisredis.set(name, Flask Redis Example)# 从Redis中读取数据name re…...
深入解析 Linux 内核内存管理核心:mm/memory.c
在 Linux 内核的众多组件中,内存管理模块是系统性能和稳定性的关键。mm/memory.c 文件作为内存管理的核心实现,承载着页面故障处理、页面表管理、内存区域映射与取消映射等重要功能。本文将深入探讨 mm/memory.c 的设计思想、关键机制以及其在内核中的作用,帮助读者更好地理…...
微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...
MODBUS TCP转CANopen 技术赋能高效协同作业
在现代工业自动化领域,MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步,这两种通讯协议也正在被逐步融合,形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...
WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...
力扣-35.搜索插入位置
题目描述 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 class Solution {public int searchInsert(int[] nums, …...
OD 算法题 B卷【正整数到Excel编号之间的转换】
文章目录 正整数到Excel编号之间的转换 正整数到Excel编号之间的转换 excel的列编号是这样的:a b c … z aa ab ac… az ba bb bc…yz za zb zc …zz aaa aab aac…; 分别代表以下的编号1 2 3 … 26 27 28 29… 52 53 54 55… 676 677 678 679 … 702 703 704 705;…...
论文阅读笔记——Muffin: Testing Deep Learning Libraries via Neural Architecture Fuzzing
Muffin 论文 现有方法 CRADLE 和 LEMON,依赖模型推理阶段输出进行差分测试,但在训练阶段是不可行的,因为训练阶段直到最后才有固定输出,中间过程是不断变化的。API 库覆盖低,因为各个 API 都是在各种具体场景下使用。…...
springboot 日志类切面,接口成功记录日志,失败不记录
springboot 日志类切面,接口成功记录日志,失败不记录 自定义一个注解方法 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/***…...
DeepSeek越强,Kimi越慌?
被DeepSeek吊打的Kimi,还有多少人在用? 去年,月之暗面创始人杨植麟别提有多风光了。90后清华学霸,国产大模型六小虎之一,手握十几亿美金的融资。旗下的AI助手Kimi烧钱如流水,单月光是投流就花费2个亿。 疯…...
