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.mzph.cn/bicheng/69373.shtml

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

相关文章

高低频混合组网系统中基于地理位置信息的信道测量算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 2.算法运行软件版本 matlab2022a 3.部分核心程序 &#xff08;完整版代码包含详细中文注释和操作步骤视频&#xff09…

ES6 类语法:JavaScript 的现代化面向对象编程

Hi&#xff0c;我是布兰妮甜 &#xff01;ECMAScript 2015&#xff0c;通常被称为 ES6 或 ES2015&#xff0c;是 JavaScript 语言的一次重大更新。它引入了许多新特性&#xff0c;其中最引人注目的就是类&#xff08;class&#xff09;语法。尽管 JavaScript 一直以来都支持基于…

1.1第1章DC/DC变换器的动态建模-1.1状态平均的概念--电力电子系统建模及控制 (徐德鸿)--读书笔记

电力电子系统一般由电力电子变换器&#xff08;滤波电路和开关&#xff09;、PWM 调制器、驱动电路、反馈控制单元构成&#xff0c;如图1-1所示。由控制理论的知识&#xff0c;电力电子系统的静态和动态性能的好坏与反馈控制设计密切相关。要进行反馈控制设计&#xff0c;首先要…

Cursor 帮你写一个小程序

Cursor注册地址 首先下载客户端 点击链接下载 1 打开微信开发者工具创建一个小程序项目 选择TS-基础模版 官方 2 然后使用Cursor打开小程序创建的项目 3 在CHAT聊天框输入自己的需求 比如 小程序功能描述&#xff1a;吃什么助手 项目名称&#xff1a; 吃什么小程序 功能目标…

react-bn-面试

1.主要内容 工作台待办 实现思路&#xff1a; 1&#xff0c;待办list由后端返回&#xff0c;固定需要的字段有id(查详细)、type(本条待办的类型)&#xff0c;还可能需要时间&#xff0c;状态等 2&#xff0c;一个集中处理待办中转路由页&#xff0c;所有待办都跳转到这个页面…

梯度下降优化算法-指数加权平均

1. 指数加权平均的定义 指数加权平均是一种对时间序列数据进行平滑处理的方法。它的核心思想是对历史数据赋予指数衰减的权重&#xff0c;最近的观测值权重较大&#xff0c;而较早的观测值权重逐渐减小。 假设有一系列观测值 x 1 , x 2 , … , x t x_1, x_2, \dots, x_t x1​…

Python3 【函数】项目实战:5 个新颖的学习案例

Python3 【函数】项目实战&#xff1a;5 个新颖的学习案例 本文包含5编程学习案例&#xff0c;具体项目如下&#xff1a; 简易聊天机器人待办事项提醒器密码生成器简易文本分析工具简易文件加密解密工具 项目 1&#xff1a;简易聊天机器人 功能描述&#xff1a; 实现一个简易…

微信小程序中实现进入页面时数字跳动效果(自定义animate-numbers组件)

微信小程序中实现进入页面时数字跳动效果 1. 组件定义,新建animate-numbers组件1.1 index.js1.2 wxml1.3 wxss 2. 使用组件 1. 组件定义,新建animate-numbers组件 1.1 index.js // components/animate-numbers/index.js Component({properties: {number: {type: Number,value…

WGCLOUD使用介绍 - 如何监控ActiveMQ和RabbitMQ

根据WGCLOUD官网的信息&#xff0c;目前没有针对ActiveMQ和RabbitMQ这两个组件专门做适配 不过可以使用WGCLOUD已经具备的通用监测模块&#xff1a;进程监测、端口监测或者日志监测、接口监测 来对这两个组件进行监控

洛谷U525376 信号干扰 (判断多个区间是否有重叠)

U525376信号干扰 题目描述 有 n n n 座信号塔&#xff0c;第 i i i 座信号塔的信号将覆盖区间 [ l i , r i ] [l_i,r_i] [li​,ri​]。 若某个点被超过一座信号塔的信号覆盖&#xff0c;则在该点会产生信号干扰。 对于信号塔区间 [ a , b ] [a,b] [a,b]&#xff0c;若建…

在无sudo权限Linux上安装 Ollama 并使用 DeepSeek-R1 模型

本教程将指导你如何在 Linux 系统上安装 Ollama&#xff08;一个本地运行大型语言模型的工具&#xff09;&#xff0c;并加载 DeepSeek-R1 模型。DeepSeek-R1 是一个高性能的开源语言模型&#xff0c;适用于多种自然语言处理任务。 DeepSeek-R1 简介 DeepSeek-R1 是 DeepSeek …

Ubuntu 安装 QGIS LTR 3.34

QGIS官方提供了安装指南&#xff1a;https://qgis.org/resources/installation-guide/#linux。大多数linux发行版将QGIS拆分为几个包&#xff1a;qgis、qgis-python、qgis-grass、qgis-plugin-grass、qgis-server&#xff0c;有的包最初安装时被跳过&#xff0c;可以在需要使用…

计算树的叶子节点,使用c语言实现

//树的数据结构 typedef struct node{ ElemType data; /*数据域*/ struct node *child, *brother; /*孩子与兄弟域 */ }Tree; //计算树的叶子节点的个数 int Leaves (Tree *root){/*计算以孩子-兄弟表示法存储的森林的叶子数*/ if(root) if(root-&…

Visio2021下载与安装教程

这里写目录标题 软件下载软件介绍安装步骤 软件下载 软件名称&#xff1a;Visio2021软件语言&#xff1a;简体中文软件大小&#xff1a;4.28G系统要求&#xff1a;Windows10或更高&#xff0c;64位操作系统硬件要求&#xff1a;CPU2GHz &#xff0c;RAM4G或更高下载链接&#…

c++贪心

本篇文章&#xff0c;我将同大家一起学习c的贪心&#xff01;&#xff01;&#xff01; 目录 第一题 题目链接 题目解析 代码原理 代码编写 第二题 题目链接 题目解析 代码原理 代码编写 第三题 题目链接 题目解析 代码原理 代码编写 第四题 题目链接 题目解…

活动回顾和预告|微软开发者社区 Code Without Barriers 上海站首场活动成功举办!

Code Without Barriers 上海活动回顾 Code Without Barriers&#xff1a;AI & DATA 深入探索人工智能与数据如何变革行业 2025年1月16日&#xff0c;微软开发者社区 Code Without Barriers &#xff08;CWB&#xff09;携手 She Rewires 她原力在大中华区的首场活动“AI &…

嵌入式C语言:结构体的多态性之结构体中的void*万能指针

目录 一、void*指针在结构体中的应用 二、实现方式 2.1. 定义通用结构体 2.2. 定义具体结构体 2.3. 初始化和使用 三、应用场景 3.1. 内存管理函数 3.2. 泛型数据结构&#xff08;链表&#xff09; 3.3. 回调函数和函数指针 3.4. 跨语言调用或API接口&#xff08;模拟…

NoteGen:记录、写作与AI融合的跨端笔记应用

在信息爆炸的时代,如何高效地捕捉灵感、整理知识并进行创作成为了许多人关注的问题。为此,我们开发了 NoteGen,一款专注于记录和写作的跨端 AI 笔记应用。它基于 Tauri 开发,利用其强大的跨平台能力支持 Mac、Windows 和 Linux 系统,并计划未来扩展到 iOS 和 Android 平台…

BUUCTF 蜘蛛侠呀 1

BUUCTF:https://buuoj.cn/challenges 文章目录 题目描述&#xff1a;密文&#xff1a;解题思路&#xff1a;flag&#xff1a; 相关阅读 CTF Wiki Hello CTF NewStar CTF buuctf-蜘蛛侠呀 BUUCTF&#xff1a;蜘蛛侠呀 MISC&#xff08;时间隐写&#xff09;蜘蛛侠呀 题目描述&am…

Web3 的核心理念:去中心化如何重塑互联网

Web3 是新一代互联网的构想&#xff0c;它的核心理念是去中心化&#xff0c;旨在打破传统互联网由大型平台主导的数据垄断&#xff0c;赋予用户更多的控制权和隐私保护。通过区块链技术和去中心化应用&#xff08;DApps&#xff09;&#xff0c;Web3 正在重塑互联网的运作方式。…