并发编程 - 线程同步(五)之原子操作Interlocked详解二
上一章我们学习了原子操作Interlocked类的几个常用方法,今天我们将继续学习该类的其他方法。

01、Exchange方法
该方法用于原子的将变量的值设置为新值,并返回变量的原始值。该方法共有14个重载方法,其中13个为常见的数据类型,有1个为泛型版本。
我们可以根据该方法可以原子更新一个变量并且可以同时获取该变量旧值这一特性,设计一个简单的锁机制,大致思路如下:
1.定义一个标志位,如果该标志位旧值为0表示当前线程获取锁,否则表示当前线程无法获取锁;
2.当线程获取锁后,可以进行业务处理,安全的处理非线程安全资源访问的代码;
3.当线程处理完业务后,释放锁,即更新标志位值为0,当前线程则退出锁,其他线程可以继续获取锁;
实现代码如下;
//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)算法,并实现一个简单的版本控制功能。
所谓版本控制就是指使用一个版本号来表示对象的状态,每次更新该对象时,我们都希望确保只有当当前版本号与我们预期的版本号一致时才能执行更新操作。否则,说明在这期间有其他线程更新了该对象,我们需要放弃当前操作或者重试。
首先我们需要构建一个版本化数据类,该类中有两个字段用于分别用于存储数据和版本号;并提供两个方法,一个方法用于获取当前数据和版本号,一个方法用于通过版本号更新数据。具体实现代码如下:
//版本化数据
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;
}
}
完成版本化类设计后,就可以使用了,我们模拟两个线程,同时获取当前版本化数据和版本号,然后同时再更新数据,具体代码如下:
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四种数据类型;
主要还是用于多线程环境下,对共享变量进行安全的原子性按位与操作,避免并发修改时可能出现的数据不一致问题。
下面看一个简单的例子:
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 的读取和写入无法跨屏障移动。后面有机会我们再详细讲解。
注:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner
并发编程 - 线程同步(五)之原子操作Interlocked详解二的更多相关文章
- Python并发编程-线程同步(线程安全)
Python并发编程-线程同步(线程安全) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 线程同步,线程间协调,通过某种技术,让一个线程访问某些数据时,其它线程不能访问这些数据,直 ...
- 33 - 并发编程-线程同步-Event-lock
目录 1 线程同步 1.1 Event 1.1.1 什么是Flag? 1.1.2 Event原理 1.1.3 吃包子 1.2 Lock 1.2.1 lock方法 1.2.2 计数器 1.2.3 非阻塞 ...
- Java线程同步的四种方式详解(建议收藏)
Java线程同步属于Java多线程与并发编程的核心点,需要重点掌握,下面我就来详解Java线程同步的4种主要的实现方式@mikechen 目录 什么是线程同步 线程同步的几种方式 1.使用sync ...
- Java并发编程(07):Fork/Join框架机制详解
本文源码:GitHub·点这里 || GitEE·点这里 一.Fork/Join框架 Java提供Fork/Join框架用于并行执行任务,核心的思想就是将一个大任务切分成多个小任务,然后汇总每个小任务 ...
- java并发编程笔记(五)——线程安全策略
java并发编程笔记(五)--线程安全策略 不可变得对象 不可变对象需要满足的条件 对象创建以后其状态就不能修改 对象所有的域都是final类型 对象是正确创建的(在对象创建期间,this引用没有逸出 ...
- C#并行编程-线程同步原语
菜鸟学习并行编程,参考<C#并行编程高级教程.PDF>,如有错误,欢迎指正. 目录 C#并行编程-相关概念 C#并行编程-Parallel C#并行编程-Task C#并行编程-并发集合 ...
- [书籍翻译] 《JavaScript并发编程》第五章 使用Web Workers
本文是我翻译<JavaScript Concurrency>书籍的第五章 使用Web Workers,该书主要以Promises.Generator.Web workers等技术来讲解Ja ...
- Java并发编程:同步容器
Java并发编程:同步容器 为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch). ...
- 【转】Java并发编程:同步容器
为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch).今天我们就来讨论下同步容器. ...
- 8、Java并发编程:同步容器
Java并发编程:同步容器 为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch). ...
随机推荐
- 基于Spring源码分析AOP的实现机制
Spring一个重要的特性就是提供了AOP,使得我们可以在原有的基础上增加我们自己的系统业务逻辑.使得我们系统业务逻辑与应用业务逻辑相分离,耦合性降低,并且大大的提高了开发的效率.Spring的AOP ...
- Java之基础语法
最近发现公司中老的项目有些是基于Java开发的, 为了更好的后期维护 不得而要好好了解一下java, 之前一直比较抵触这门语言,现在随着了解的语言(PHP,Python,golang,shell,no ...
- uni.showModel内容换行
前情 最近在做小程序项目,选用有是当前比较火的uniapp技术栈,在产品项目中用到不少的需要引导用户确认后才继续操作的弹框. 为什么想到我去换行? 其实showModel弹框的content是支持自动 ...
- C++ builder 10.2 x64程序使用typeid获取vcl类名时异常
C++ builder 10.2 x64程序使用typeid获取vcl类名时异常 比如: const std::type_info &t= typeid(TForm1); 那么t的name() ...
- Winform 使用WebView2 开发现代应用
使用 WebView2 开发现代应用 WebView2 是 Microsoft 提供的一种嵌入式浏览器控件,基于 Edge (Chromium) 引擎.它允许开发者将现代 Web 技术(如 HTML. ...
- 中电金信:技术实践|Flink多线程实现异构集群的动态负载均衡
导语:Apache Flink是一个框架和分布式处理引擎,用于对无界和有界数据流进行有状态计算.本文主要从实际案例入手并结合作者的实践经验,向各位读者分享当应用场景中异构集群无法做到负载均衡时,如何通 ...
- 使用Java将视频中某一帧抽取为封面图片
由于业务需求需将视频中的某帧进行截取作为该视频封面,网上太多教程过于复杂麻烦,经本人研究发现可以使用Java调用FFmpeg来进行处理. /** * 获取指定的视频文件后进行封面截图为png并保存到指 ...
- Qt编写的项目作品11-带频谱的音乐播放器
一.功能特点 可获取整个声音文件采样值数据 可实时获取当前播放位置的采样值数据 可设置采样的步长和数量 可开始播放/暂停播放/停止播放 多线程处理,超流畅 可设置当前播放位置 可设置和调节音量 支持任 ...
- [转]Clion中如何使用矩阵库eigen
CMakelist文件内容如下: cmake_minimum_required(VERSION 3.2) project(PISO) set(CMAKE_CXX_STANDARD 14) includ ...
- Javascript中不同的<script.../>元素中变量或函数的作用范围的说明
在同一个<script.../>元素中,Javascript允许先调用函数,然后再定义该函数:但是在不同的<script.../>元素中,必须先定义函数,再调用该函数,也就是说 ...