C#中的原子操作Interlocked,你真的了解吗?
阅读目录
一、背景
这个标题起的有点标题党的嫌疑[捂脸],这个事情的原委是这样的,有个Web API的站点在本地使用Release模式Run的时候出现问题,但是使用Debug模式则不会。通过打日志定位到问题在如下的这个代码这里:
private static int _flag;
public void ExactlyOnceMethod()
{
var original = Interlocked.Exchange(ref _flag, );
if (original == _flag)
{
// 1.重复进入
}
else
{
// 2.第一次进入
}
}
理论上,会有一次请求进入到2中,但是实际问题是全部都进入到了1中。
二、代码描述
这个代码很简单,就做了2个事情,1是使用Interlocked.Exchange将_flag变量进行赋值。2是将Interlocked.Exchange操作后返回的原始值与_flag变量进行对比,如果相等说明这个变量已经被修改过了,表示这里是重入了。如果不是则说明第一次进入此方法。
关于Interlocked.Exchange的解释,见微软官网文档,传送门在此:https://msdn.microsoft.com/zh-cn/library/d3fxt78a.aspx
三、越分析越黑暗
好了,咋一看了好几分钟,也没看出有什么不妥的地方,那么首先就往多线程问题上考虑了。但是这里唯一的共享变量就是_flag,走的又是CAS操作,在这里不存在多线程问题。而且结合日志输出,的确这个方法就是只执行了一次。仔细的再看了一遍官方文档中的内容,见下图1。我发现示例代码中的写法和我上面贴的代码是不一样的,这里并没有重用变量usingResource,而且直接将比较的对象变成了一个常量0。

【图1】
带着好奇,我去翻阅了下.Net Framework的源码。传送门在此 http://referencesource.microsoft.com/#mscorlib/system/threading/interlocked.cs,52be0cc9b3954ae9 。但是它直接是个extern方法,见下图2:

【图2】
这里又陷入了一个困境,现在线索断了。查阅了一些资料,MethodImplOptions.InternalCall 表明这个方法的实现在微软开源的sscli中可以找到答案(原文地址 http://bbs.csdn.net/topics/330019064 中的5楼回复)。但是经各方查找,目前已经找不到源码所在地了,据说是.Net Framework 2.0时代的产物。
OK,那我就想看下汇编代码试试。下面是反编译出的汇编代码:
var original = Interlocked.Exchange(ref _flag, );
00DC35EF mov ecx,5F2DFCCh //将5F2DFCCh地址上的数据放入寄存器ecx
00DC35F4 mov edx, //将1放入寄存器edx
00DC35F9 call 70B95330 //调用70B95330地址上的方法
00DC35FE mov dword ptr [ebp-48h],eax //将寄存器eax的数据 保存到地址ebp-48h的双字型指针上
00DC3601 mov eax,dword ptr [ebp-48h] //将地址ebp-48h的双字型指针上的数据放入寄存器eax(可以理解上上一步的反向操作)
00DC3604 mov dword ptr [ebp-40h],eax //将寄存器eax的数据 保存到地址ebp-40h的双字型指针上
if (original == _flag)
00DC3607 mov eax,dword ptr [ebp-40h] //将地址ebp-40h的双字型指针上的数据放入寄存器eax
00DC360A cmp eax,dword ptr ds:[5F2DFCCh] //比较地址ds:[5F2DFCCh]的双字型指针上的数据和寄存器eax中的数据。 这里开始下面的代码不是我们的讨论点了,就不翻译了
00DC3610 setne al
00DC3613 movzx eax,al
00DC3616 mov dword ptr [ebp-44h],eax
00DC3619 cmp dword ptr [ebp-44h],
00DC361D jne 00DC3624
这里的5F2DFCCh其实就是_flag。我们可以看到在真正做这个Interlocked.Exchange操作的时候,并没有直接去修改5F2DFCCh地址上的数据,但是在做cmp操作的时候由于我们比较的对象是_flag变量,所以还是继续使用了5F2DFCCh地址上的数据。也就是说:CPU运算在寄存器中操作数据,但是我们用于判断的变量是个静态全局变量,持有的是这个引用地址。那么是不是可以这么来理解:【如果说Interlocked的内部操作与当前上下文使用的并不是同一个CPU核心】,那么这个“判断依据”并不是像代码上写的这样,因为我们预期是肯定一样的(变量都是同一个)。理由是做Interlocked的时候在CPU1的高速缓存中,另一个在CPU2上操作加载的数据还是内存中的。其中CPU1往内存同步数据(将寄存器中的值赋值给_flag这个全局变量)有一个非常短的时间差。如果是这样的话,也就能解释为什么会有下面的3种情况出现:
1.在有的机器上是没问题的,在有的机器上是有问题的。
2.在Debug模式下是没问题的,在Release模式下是有问题的。
3.在if语句之前增加一条日志记录到物理文件中也是没问题的。
依据这个推测的话,原因就是因为这个时间差的耗时和所在机器的硬件配置环境都有关系。只要这个“赋值”操作所用时间 < 代码执行到if所需的时间,那么就不会出现问题。根据这个结论也能得出解决方案,就是让这个表达式成立即可,哪怕就是简单粗暴的Sleep1毫秒都行。笔者建议的解决方案有2种:
方案1:是给这个全局变量增加volatile关键字即可,关键字的说明请看这里(https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/volatile)。
方案2:参照官方的示例写法,将_flag替换为常量来做比较,比如这里可以更改成original == 0 即可。
四、结语
总结一下:
使用Interlocked做的CAS本身是一个CPU操作。数据是放在CPU的寄存器中做的交换。但是我们判断的变量是个静态全局变量,持有的是这个引用地址。
也就是出现问题的流程是:
1.从传入的ref引用地址加载数据到CPU寄存器
2.寄存器中做交换并且返回原始值,但是更新引用地址的操作并不是在这个上下文中的同步操作。
3.然后我们比较的时候,左侧原始值肯定为0,但是流程1中的变量在非常短的时间内也是原始值为0(如图3)。导致了这个问题的产生。

【图3】
强调一下,这个结论也是建立在【如果说Interlocked的内部操作与当前上下文使用的并不是同一个CPU核心】的猜测下,这方面资料实在是找不到也无法进一步验证,所以我也不是敢100%确定是否正确。如果哪位小伙伴能够来个明确的解惑欢迎在下面留言~
在分析该问题的过程中,参考了以下几位小伙伴的思想成果,感谢分享:
http://286.iteye.com/blog/2295165
http://www.cnblogs.com/5iedu/p/4719625.html
http://blog.csdn.net/hsuxu/article/details/9467651
作者:Zachary
出处:https://zacharyfan.com/archives/262.html
▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描右侧的二维码~。
定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。
如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。
如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。
C#中的原子操作Interlocked,你真的了解吗?的更多相关文章
- 原子操作 Interlocked系列函数
上一篇<多线程第一次亲密接触 CreateThread与_beginthreadex本质区别>中讲到一个多线程报数功能.为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是 ...
- (转)原子操作 Interlocked系列函数
上一篇<多线程第一次亲密接触 CreateThread与_beginthreadex本质区别>中讲到一个多线程报数功能.为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是 ...
- 多线程面试题系列(3):原子操作 Interlocked系列函数
上一篇中讲到一个多线程报数功能.为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是否运行出错.这也非常类似于统计一个网站每天有多少用户登录,每个用户登录用一个线程模拟,线程运行时会将 ...
- 秒杀多线程第三篇 原子操作 Interlocked系列函数
上一篇<多线程第一次亲密接触 CreateThread与_beginthreadex本质区别>中讲到一个多线程报数功能.为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是 ...
- 多线程--原子操作 Interlocked系列函数
[转]原文地址:http://blog.csdn.net/morewindows/article/details/7429155 线程同步与互斥: 互斥主要指多个线程不能同时访问一个资源,如打印机就是 ...
- Java学习技术分享:Java中的原子操作
学习java需要有一套完整的学习线路,需要有条理性,当下学习java已经有一段时间了,由当初的懵逼状态逐渐好转,也逐渐养成了写技术学习笔记的习惯,今天总结了一下java中的原子操作. 1.Java中的 ...
- OpenGL中的原子操作需要注意的地方
OpenGL中的原子操作需要注意的地方 仔细阅读看画红线的部分
- Java中的原子操作类
转载: <ava并发编程的艺术>第7章 当程序更新一个变量时,如果多线程同时更新这个变量,可能得到期望之外的值,比如变量i=1,A线程更新i+1,B线程也更新i+1,经过两个线程操作之后可 ...
- 【Java并发】Java中的原子操作类
综述 JDK从1.5开始提供了java.util.concurrent.atomic包. 通过包中的原子操作类能够线程安全地更新一个变量. 包含4种类型的原子更新方式:基本类型.数组.引用.对象中字段 ...
随机推荐
- js变量以及其作用域详解
详见: http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp73 一.变量的类型 Javascript和Java.C这些语言不同 ...
- my new start
my new start in blog csdn : today i formally migrate my personal technical blog from sina to here in ...
- YYHS-怎样更有力气
题目描述 OI大师抖儿在夺得银牌之后,顺利保送pku.这一天,抖儿问长者:"我虽然已经保送了,但我的志向是为国家健康工作五十年.请问我应该怎样变得更有力气?" 长者回答:&quo ...
- KVM网页管理工具WebVirtMgr部署
KVM-WebVirtMgr 0ther https://github.com/retspen/webvirtmgr/wiki System Optimization(Only CentOS6.X) ...
- 团队作业8----第二次项目冲刺(Beta阶段) 第六天
BETA阶段冲刺第六天 1.小会议ing 2.每个人的工作 (1)昨天已完成的工作 重复部分可以用红色字体显示 (2) 今天计划完成的工作 (3) 工作中遇到的困难: 尤少辉:在测试的时候,当队友提出 ...
- 微信小程序scroll标签的测试
一:testscroll.wxml的代码如下.testview.js自动生成示例代码 //testscroll.wxml <view class="section__title&quo ...
- 201521123109《java程序设计》第八周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结集合与泛型相关内容. 1.2 选做:收集你认为有用的代码片段 2. 书面作业 本次作业题集集合 List中指定元素的删除(题目4-1 ...
- 201521123096《Java程序设计》第六周学习总结
1. 本周学习总结 1.1 面向对象学习暂告一段落,请使用思维导图,以封装.继承.多态为核心概念画一张思维导图,对面向对象思想进行一个总结. 2. 书面作业 (1)clone方法 1.1 Object ...
- 201521123010 《Java程序设计》第3周学习总结
1. 本周学习总结 2. 书面作业 1.代码阅读 public class Test1 { private int i = 1;//这行不能修改 private static int j = 2; p ...
- 201521123074 《Java程序设计》第1周学习总结
1. 本章学习总结第一周学习总结: 学习了java语言的历史与发展,运行过程,安装了eclipse编程平台,试着编写了java程序. 2. 书面作业: Q 1.为什么java程序可以跨平台运行?执行j ...