Interlocked.CompareExchange这玩意用法被讲的很烂了,为何再次被提及,主要看了下官网对其使用描述可能对于部分童鞋来讲具备一定迷惑性,需我们进一步消化下,话不多讲,我们来看看吧~~~
浅谈如标题
关于啥时候用此语法,这.....此处省略若干字,上代码
- var location = 1;
- var value = 3;
- var compared = 1;
- Interlocked.CompareExchange(ref location, value, compared);
若原始值(location)与比较值(compared)一致,则将当前值(value)替换原始值。如图上述,此时原始值将为3,毫无疑问,我们很清楚这是基本用法
问题来了,该语法既可比较基本类型也可比较引用对象
当比较对象时,官方还特此备注注意:
比较对象的引用相等性,而不是值相等性。结果,两个具有相同值类型(例如,整数3)的装箱实例始终看起来不相等,并且不执行任何操作。不要将此重载用于值类型。
初次看,稍不注意的迷惑性来了,解释如下截图
从描述上看,一部分童鞋是不是会认为,原始值为空,则会引发空引用异常,如下代码
- object o1 = null;
- object o2 = null;
- Interlocked.CompareExchange(ref o1, o2, null);
运行上述代码将不会抛出空引用异常,这是为何?难道官方解释有误,接下来我们深入探讨下
深谈如标题
若对C语言有所了解,则不会存在疑惑,官方解释为空指针(不是空),而我们代码是空引用,二者不可同日而语
C#中对于引用类型,定义现有的变量必将存在引用,所以在C#中不可能存在空引用,所以我们是不是可以认为必然不会存在抛出空引用异常
官方解释为空指针和null其实并不是同一个概念,如此一解释,极易引起概念混淆,还不如去掉,显得有点多余
再想想,也不是那么绝对,个人以为,至少在托管情况下理论上应该不会抛出任何异常,非托管情况下可能没有保证,或者通过IL操作底层,也会触发上述空引用异常
综上个人理解,官方解释谈不上迷惑性,只是好像有点会引起概念混淆,让部分童鞋以为不能传递空,希望没将各位绕晕图片
那么我们在哪些场景下会用到上述原始值为空的情况呢?比如确保对象初始化,如下:
- public class Order
- {
- public Address Address
- {
- get
- {
- return CreateAddress();
- }
- }
- private Address CreateAddress() => new Address();
- }
- public class Address
- {
- }
当我们在Order内部使用Address时,确保其实例已完全初始化,如下:
- static Address EnsureAddressInitialized()
- {
- var order = new Order();
- Func<Order, Address> func = f => f.Address;
- Address target = null;
- var result = Interlocked.CompareExchange(ref target, func(order), null);
- Console.WriteLine(result == null);
- Console.WriteLine(target);
- return target!;
- }
上述方法返回对象实例时,我们使用C# 8.0语法表明对象实例绝不可能为空
该语法有返回值,那打印结果是否和替换后的原始值一样呢?不是,除了替换原始值外,针对所有情况,返回值都是最初原始值即旧值。
据我所知,该语法底层直接操作CPU处理器指令,当然也是原子性操作,即便是操作系统也无法执行中断操作,线程可以在指令执行之前被抢占,但在指令执行期间不会被抢占,换言之,绝不会出现,当原始值和比较值比较相等时,而另一线程指令更改原始值的情况
基于上述理论,所以才有了网上大多数通过循环方式对其返回结果赋最新原始值实现并发无锁修改操作
- public static Address EnsureAddressInitialized(
- ref Address target,
- Order order, Func<Order, Address> func)
- {
- if (target != null)
- {
- return target;
- }
- while (Interlocked.CompareExchange(ref target, func(order), null) == null)
- {
- break;
- }
- return target;
- }
为避免上述没必要的循环操作,同时也为避开CPU缓存,我们进一步进行代码优化,通过使用Volatile关键字获取内存最新存储数据,最终演变成如下这般
- public static Address EnsureAddressInitialized(
- ref Address target,
- Order order, Func<Order, Address> func)
- {
- var tmp = Volatile.Read(ref target);
- if (tmp != null)
- {
- return tmp;
- }
- Interlocked.CompareExchange(ref target, func(order), null);
- return target!;
- }
概念的混淆可能会存在使用上的疑惑,同时我们基于理论逐步优化,实现并发无锁修改操作