阅读目录

一、背景

  这个标题起的有点标题党的嫌疑[捂脸],这个事情的原委是这样的,有个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,你真的了解吗?的更多相关文章

  1. 原子操作 Interlocked系列函数

    上一篇<多线程第一次亲密接触 CreateThread与_beginthreadex本质区别>中讲到一个多线程报数功能.为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是 ...

  2. (转)原子操作 Interlocked系列函数

    上一篇<多线程第一次亲密接触 CreateThread与_beginthreadex本质区别>中讲到一个多线程报数功能.为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是 ...

  3. 多线程面试题系列(3):原子操作 Interlocked系列函数

    上一篇中讲到一个多线程报数功能.为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是否运行出错.这也非常类似于统计一个网站每天有多少用户登录,每个用户登录用一个线程模拟,线程运行时会将 ...

  4. 秒杀多线程第三篇 原子操作 Interlocked系列函数

    上一篇<多线程第一次亲密接触 CreateThread与_beginthreadex本质区别>中讲到一个多线程报数功能.为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是 ...

  5. 多线程--原子操作 Interlocked系列函数

    [转]原文地址:http://blog.csdn.net/morewindows/article/details/7429155 线程同步与互斥: 互斥主要指多个线程不能同时访问一个资源,如打印机就是 ...

  6. Java学习技术分享:Java中的原子操作

    学习java需要有一套完整的学习线路,需要有条理性,当下学习java已经有一段时间了,由当初的懵逼状态逐渐好转,也逐渐养成了写技术学习笔记的习惯,今天总结了一下java中的原子操作. 1.Java中的 ...

  7. OpenGL中的原子操作需要注意的地方

    OpenGL中的原子操作需要注意的地方 仔细阅读看画红线的部分

  8. Java中的原子操作类

    转载: <ava并发编程的艺术>第7章 当程序更新一个变量时,如果多线程同时更新这个变量,可能得到期望之外的值,比如变量i=1,A线程更新i+1,B线程也更新i+1,经过两个线程操作之后可 ...

  9. 【Java并发】Java中的原子操作类

    综述 JDK从1.5开始提供了java.util.concurrent.atomic包. 通过包中的原子操作类能够线程安全地更新一个变量. 包含4种类型的原子更新方式:基本类型.数组.引用.对象中字段 ...

随机推荐

  1. ios初体验< 运用属性传值,登录>

    注意:ViewController.m文件 // 在第一个页面中,创建一个简单的登录页面,并且添加两个属性 1 #import "ViewController.h" #import ...

  2. Swiper 滑动

    1.http://www.swiper.com.cn/download/  下载Swiper.JS  Swiper.CSS 2.引入项目,添加html <div class="cont ...

  3. 转: 【Java并发编程】之十四:图文讲述同步的另一个重要功能:内存可见性

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/17288243 加锁(synchronized同步)的功能不仅仅局限于互斥行为,同时还存在另 ...

  4. 个人作业3——个人总结(Alpha阶段)

    个人总结 在Alpha冲刺阶段中,我们团队基本完成了项目的大致基础框架,还有很多不足需要更多的时间来让我们做得更好. 对我个人而言,Alpha冲刺阶段是一个强度很大的阶段,每天都在吸收新的知识,团队也 ...

  5. 201521123107 《Java程序设计》第2周学习总结

    第2周作业-Java基本语法与类库 1.本周学习总结 要点主要有: (1)String类 String类是本周的一个重点,String类的对象是不可变的,即String对象后就在内存中开辟了一个字符串 ...

  6. 201521123067 《Java程序设计》第7周学习总结

    201521123067 <Java程序设计>第7周学习总结 1. 本周学习总结 以你喜欢的方式(思维导图或其他)归纳总结集合相关内容. 2. 书面作业 Q1.ArrayList代码分析 ...

  7. 201521123088《JAVA程序设计》第5周学习总结

    1. 本周学习总结 1.1 尝试使用思维导图总结有关多态与接口的知识点. 2. 书面作业 阅读:Child压缩包内源代码 1.1 com.parent包中Child.java文件能否编译通过?哪句会出 ...

  8. Win8打开chm右侧空白解决方法

    Win8下打开CHM文件,左侧有目录,但是右侧空白.而且打开的时候,还弹出很多IE窗口. 感觉应该不是文件本身的问题.下面是我的解决方法,其他系统也可以试一试. 最初打开文件如下 首先:1,右键关联c ...

  9. 201521123062 《Java程序设计》第14周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多数据库相关内容. 2. 书面作业 1. MySQL数据库基本操作 建立数据库,将自己的姓名.学号作为一条记录插入.(截图,需出现自 ...

  10. 201521123087《Java程序设计》第12周学习总结

    1. 本周学习总结 2. 书面作业 将Student对象(属性:int id, String name,int age,double grade)写入文件student.data.从文件读出显示. 1 ...