编程博客
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 发布
-
+
首页
08、并发编程 - 线程同步(五)之原子操作 Interlocked 详解二
上一章我们学习了原子操作 Interlocked 类的几个常用方法,今天我们将继续学习该类的其他方法。  # ***01***、Exchange 方法 该方法用于原子的将变量的值设置为新值,并返回变量的原始值。该方法共有 14 个重载方法,其中 13 个为常见的数据类型,有 1 个为泛型版本。 我们可以根据该方法可以原子更新一个变量并且可以同时获取该变量旧值这一特性,设计一个简单的锁机制,大致思路如下: 1.定义一个标志位,如果该标志位旧值为 0 表示当前线程获取锁,否则表示当前线程无法获取锁; 2.当线程获取锁后,可以进行业务处理,安全的处理非线程安全资源访问的代码; 3.当线程处理完业务后,释放锁,即更新标志位值为 0,当前线程则退出锁,其他线程可以继续获取锁; 实现代码如下; ```csharp //0 表示未锁定,1 表示锁定 private static long _exchangeValue = 0; public static void ExchangeRun() { var rnd = new Random(); //启动10个线程 var threads = new Thread[10]; for (var j = 0; j < threads.Length; j++) { threads[j] = new Thread(ModifyExchangeValue); threads[j].Start(); //等待一段随机的时间后再开始启动另一个线程 Thread.Sleep(rnd.Next(0, 100)); } } static void ModifyExchangeValue() { //更新_exchangeValue为1,同时获取_exchangeValue旧值 var oldExchangeValue = Interlocked.Exchange(ref _exchangeValue, 1); //如果旧值为0,表示该逻辑未被其他线程占用 if (0 == oldExchangeValue) { //当前线程开始锁定该代码块,其他线程无法进入 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} 线程 进入锁"); //模拟一些工作 //这里可以实现安全的处理非线程安全资源访问的代码 Thread.Sleep(100); //释放锁 Interlocked.Exchange(ref _exchangeValue, 0); //当前线程释放完锁,其他线程可以进入该代码块 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} 线程 退出锁"); } else { Console.WriteLine($" {Thread.CurrentThread.ManagedThreadId} 线程 无法进入锁"); } } ``` 我们可以看看执行结果:  可以发现当一个线程获取锁后,其他线程则无法再获取该锁,只有当前线程处理完成业务退出改锁后,其他线程才可以继续获取该锁。 # ***02***、Exchange方法 该方法是 Exchange 方法的泛型版本,具体使用可以参考上一节。 # ***03***、CompareExchange 方法 该方法用于原子的比较第一个变量和第三个变量是否相等,如果相等,则将第一个变量替换为第二个变量值,并返回第一个变量的原始值;该方法也有 14 个重载方法,其中 13 个为常见的数据类型,有 1 个为泛型版本。 可以理解为该方法比 Exchange 方法多了一个条件判断。因此该方法可以应用于更复杂的业务场景。 下面我们就使用该方法实现 CAS(Compare and Swap)算法,并实现一个简单的版本控制功能。 所谓版本控制就是指使用一个版本号来表示对象的状态,每次更新该对象时,我们都希望确保只有当当前版本号与我们预期的版本号一致时才能执行更新操作。否则,说明在这期间有其他线程更新了该对象,我们需要放弃当前操作或者重试。 首先我们需要构建一个版本化数据类,该类中有两个字段用于分别用于存储数据和版本号;并提供两个方法,一个方法用于获取当前数据和版本号,一个方法用于通过版本号更新数据。具体实现代码如下: ```csharp //版本化数据 public class VersionedData<T>(T data) { private T _data = data; private long _version = 0; //获取当前数据和版本号 public (T Data, long Version) GetData() { return (_data, _version); } //基于版本号尝试更新数据 public bool TryUpdateData(T data, long expectedVersion) { //如果_version与预期版本号相同, //则对预期版本号加1后再替换为_version, //同时返回_version旧值 var oldVersion = Interlocked.CompareExchange(ref _version, expectedVersion + 1, expectedVersion); //如果_version旧值与预期版本号相同 if (oldVersion == expectedVersion) { //则版本号匹配,更新数据 _data = data; return true; } //否则版本号不匹配,更新失败 return false; } } ``` 完成版本化类设计后,就可以使用了,我们模拟两个线程,同时获取当前版本化数据和版本号,然后同时再更新数据,具体代码如下: ```csharp public static void CompareExchangeRun() { var versionedData = new VersionedData<string>("初始化数据"); //线程 1 尝试更新数据 var thread1 = new Thread(ModifyCompareExchangeValue); //线程 2 尝试更新数据 var thread2 = new Thread(ModifyCompareExchangeValue); thread1.Start(versionedData); thread2.Start(versionedData); thread1.Join(); thread2.Join(); //最终结果 var (finalData, finalVersion) = versionedData.GetData(); Console.WriteLine($"最终数据为 [{finalData}], 最终版本号为 [{finalVersion}]"); } static void ModifyCompareExchangeValue(object param) { var threadId = Thread.CurrentThread.ManagedThreadId; var versionedData = (VersionedData<string>)param; var (data, version) = versionedData.GetData(); Console.WriteLine($"线程 {threadId} : 当前数据为 [{data}], 当前版本号为 [{version}]"); Console.WriteLine("---------------------------------------------------"); var newData = $"线程 {threadId} 数据"; var success = versionedData.TryUpdateData(newData, version); Console.WriteLine($"线程 {threadId} 更新数据: [{(success ? "成功" : "失败")}]"); Console.WriteLine($" 数据预期更新为:[{newData}]"); Console.WriteLine($" 版本号预期更新为:[{version + 1}]"); Console.WriteLine("---------------------------------------------------"); } ``` 我们来看看执行结果:  通过结果可以发现只有 1 个线程更新成功了。 # ***04***、CompareExchange方法 该方法是 CompareExchange 方法的泛型版本,具体使用可以参考上一节。 # ***05***、And 方法 该方法用于原子的对两个变量进行按位与操作,将第一个变量替换为操作结果,并返回第一个变量的原始值;该方法同样有 4 个重载方法,分别为 long、ulong、int 和 uint 四种数据类型; 主要还是用于多线程环境下,对共享变量进行安全的原子性按位与操作,避免并发修改时可能出现的数据不一致问题。 下面看一个简单的例子: ```csharp public static void AndRun() { var a = 10; // 二进制: 1010 var b = 5; // 二进制: 0101 var oldA = Interlocked.And(ref a, b); //1010 & 0101 = 0000 = 0 Console.WriteLine("操作后的值: " + a); Console.WriteLine("返回的结果: " + oldA); } ``` 看看执行结果;  可以理解为就是两个数进行按位与运算,并且可以原子的更新原值,并返回原始值。 # ***06***、Or 方法 该方法用于原子的对两个变量进行按位或操作,将第一个变量替换为操作结果,并返回第一个变量的原始值;该方法也有 4 个重载方法,分别为 long、ulong、int 和 uint 四种数据类型; 具体使用可以参考 And 方法。 # ***07***、MemoryBarrier 方法 该方法用于强制执行内存屏障,作用范围当前线程,无返回值。后面有机会我们再详细讲解。 # ***08***、MemoryBarrierProcessWide 方法 该方法用于提供进程范围的内存屏障,确保任何 CPU 的读取和写入无法跨屏障移动。后面有机会我们再详细讲解。
个人天使
2025年2月10日 10:29
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码