并发编程 - 线程同步(五)之原子操作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 的读取和写入无法跨屏障移动。后面有机会我们再详细讲解。

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

相关推荐
IT规划师2 天前
并发编程 - 线程同步(四)之原子操作Interlocked详解一
多线程·并发编程·线程同步
IT规划师3 天前
并发编程 - 线程同步(三)之原子操作Interlocked简介
多线程·并发编程·线程同步
charlie1145141914 天前
高阶开发基础——快速入门C++并发编程6——大作业:实现一个超级迷你的线程池
开发语言·c++·并发编程·基础学习
@Java小牛马5 天前
Redis真的是单线程的吗?
数据库·redis·缓存·reactor·单线程·多线程
桦说编程6 天前
CompletableFuture 超时功能有大坑!使用不当直接生产事故!
java·性能优化·函数式编程·并发编程
自信不孤单7 天前
Linux线程安全
linux·多线程·条件变量·线程安全·同步··互斥
fly spider10 天前
多线程-线程池的使用
java·面试·线程池·多线程·juc
桦说编程10 天前
如何安全发布 CompletableFuture ?Java9新增方法分析
java·性能优化·函数式编程·并发编程
IT规划师10 天前
并发编程 - 线程同步(二)
多线程·并发编程·线程同步