前阵子写静态lib导出单实例多线程安全API时,出现了CRITICAL_SECTION初始化太晚的问题,之后查看了错误的资料,引导向了错误的理解,以至于今天凌晨看到另一份代码,也不多想的以为singletone double check会出bug,本文做下记录备忘。
   相关知识点:Singleton Double Check、多线程下的局部Static对象、静态Lib中的全局对象、无锁编程。

一、singleton double check

SingleInstance* volatile g_instance = NULL;
cswuyg::MyCritical g_cs;
SingleInstance* GetInstance()
{
if (g_instance == NULL)
{
cswuyg::Lock<> lock(g_cs);
if (g_instance == NULL)
{
g_instance = new SingleInstance;
}
}
return g_instance;
}
    这样的代码在vs2005 IDE下(不考虑全局对象的初始化)没有问题。之前只略看他人的文章,不思考,误以为:g_instance = new SingleInstance ; 这句在线程A的执行会被线程B g_instance == NULL的判断打断,导致线程B返回的g_instance是一个半成品。实际上不会,因为volatile保证了指令的执行顺序,g_instance的赋值是在内存分配、构造函数执行之后做的,而且赋值是原子操作,完全没有问题。
    特别注意,g_instance变量必须加上volatile。
    volatile一般有两个好处:1是使得多个线程直接操作内存,变量被某个线程改变后其它线程也可以及时看到改变后的值;2是阻止编译器优化操作volatile变量的指令执行顺序。这里如果不使用它,就可能导致编译器调整汇编指令的顺序,分配完内存就直接把地址赋值给g_instance指针,后面再调用构造函数,它这样调整的理由可能是这样子:分配到的内存指针在后续的执行中没有被修改,先赋值给g_instance和晚赋值给g_instance没有区别,这就导致了半成品对象的产生。
    volatile还有另外的好处。除了关注编译器优化之外,还需要关注CPU的指令顺序调整,必须阻止它。vs2005以后的编译器对volatile关键字做了CPU层面的支持,使用了acquire、release、fence语义,所以使用volatile可以解决编译器的优化和CPU的指令调整问题。而vs2005之前的编译器,则需要使用原子操作对g_instance赋值,或者是使用MemoryBarrier宏,这是CPU指令的支持。
补充1:
MemoryBarrier方面的资料:
http://stackoverflow.com/questions/2484980/why-is-volatile-not-considered-useful-in-multithreaded-c-or-c-programming
http://stackoverflow.com/questions/23359265/is-memory-barrier-needed-in-this-situation-or-just-a-volatile
http://www.cnblogs.com/rocketfan/archive/2009/12/05/1617759.html
上边都强调了volatile跟MemoryBarrier不一样~ 但仅限于非MSVC环境。
下面是来自MDSN的资料:
msdn上搜索memory barrier可以发现这篇文章:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms684208(v=vs.85).aspx
MemoryBarrier宏
http://msdn.microsoft.com/en-us/library/windows/desktop/ms686355(v=vs.85).aspx
这篇文章表示,vs2005以后的volatile已经包含了memory barrier的功能,并介绍了有几类函数是不会被调换执行顺序的。
http://stackoverflow.com/questions/19652824/why-can-memorybarrier-be-implemented-as-a-call-to-xchg/19652910#19652910
它介绍了为什么x86机器上的xchg可以消灭掉CPU的reorder。
《程序员的自我修养》一书在第一章的科普部分也谈到了过度优化的问题,谈及volatile和barrier。

二、导出Lib中慎用全局对象 & 原子操作代替锁

    我的Lib的导出API提供的数据只需要获取一次就够了,不能多次获取,所以它必须是单实例的、多线程安全的,再考虑到不能浪费频繁的锁消耗,很直接的做法便是用singleton double check。
    首先我选择使用临界区实现锁,而临界区在API被调用之前需要先初始化,于是定义一个Lock封装了临界区的初始化,什么时候初始化?必须是全局对象,如果为定义局部static对象会导致多线程不安全
    static对象不是多线程安全的:
    从上图的汇编指令可以看到static对象的构造函数是否被执行的判断逻辑:
1、通过标识值判断是否该执行构造函数(这里的构造函数内联了);
2、执行构造函数,首先把标志值置位。
     有可能多个线程都同时通过了1的判断,导致构造函数被多次执行。
     使用了全局对象之后发现也不可行:导出函数依赖全局对象的初始化,虽然全局对象会在main函数之前初始化,但初始化时机还是可能太晚了,譬如这种情况:lib的使用者也定义了全局对象,并且初始化得更早,使用者的全局对象构造函数里调用了lib的导出函数,导出函数使用了还没初始化的临界区全局对象导致崩溃,更麻烦的是,使用者的dump捕获机制是在main函数里初始化的,生效得太晚,导致dump无法捕获,使这个crash更加隐蔽。C++的全局对象应该尽量少用。exe里面如果使用了全局对象,则需要保证dump捕获机制对所有的代码都生效。
    既然临界区初始化问题无法解决,局部static对象、全局对象都无法使用,需要找到一个不需要初始化又能实现锁的方法:那就是原子操作。
    单纯的原子操作并没有锁的功能,需要配合上:if + while + Sleep(当然,也可以说是if + while,不去Sleep也可以)。回头网上一搜索,这里的原子操作,也是各种“无锁XX”实现的根基,无锁XX,让我重新发明了一回。
代码如下:
SingleInstance* volatile g_instance;
LONG volatile g_for_lock; SingleInstance* GetInstance()
{
if (g_instance == NULL)
{
LONG pre_value = ::InterlockedExchange(&g_for_lock, );
if (pre_value != )
{
while(g_instance == NULL)
{
::Sleep();
}
}
if (g_instance == NULL)
{
g_instance = new SingleInstance;
}
}
return g_instance;
}

  全局的g_for_lock在PE文件装入内存时就初始化为0,所以不存在初始化问题;InterlockedExchange 适用于xp、win7、win8,不存在系统限制;多个线程同时调用InterlockedExchange,只能有一个线程得到0,保证只初始化一次,其余线程进入while循环等待,直到g_point非空。问题不逼你,你就不会想到还有这么好的实现思路 :)

使用原子操作还可以很容易的实现临界区锁的功能,这里就不说了。

补充2:
有时候,是否我们并不需要去考虑多线程?譬如,我让singleton对象只在main函数执行之前生成,这时候只能是单线程的,但要慎重,因为全局对象带来的问题可能比singleton double check要麻烦很多。对于我这次的lib来说,是不能保证singleton对象一定在main函数执行之前使用的,所以这个解决方案无法实施。

三、PE文件中的Lib库全局变量

    像上边定义的全局变量,如果DLL和EXE都使用这个lib,它们各自有一份独立的全局变量。
 

Double Check 相关资料(主要是说执行指令调整导致在多核机器上某个线程会返回半成品对象,导致DoubleCheck思路失效):

http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

http://zh.wikipedia.org/wiki/%E5%8F%8C%E9%87%8D%E6%A3%80%E6%9F%A5%E9%94%81%E5%AE%9A%E6%A8%A1%E5%BC%8F

2014.3.8补充:

抛出异常问题:对于导出API还需要注意不能影响到使用者进程的运行,所以不能抛出exception,需要这么做:1、在API实现处,使用__try...__except把所有逻辑封起来;2、在API的入口处设置非法参数、纯虚函数调用错误处理(_invalid_parameter_handler、_set_purecall_handler),并在出口处还原以便不修改外界设置,这里要解决的是CRT抛出的错误,它跟SEH没关系,所以使用__try...__except无法catch住,这两函数的相关知识参考:《windows下的dump捕获》http://www.cnblogs.com/cswuyg/p/3207576.html。

2014.3.13补充:

编译依赖问题:EXE C依赖Lib B,Lib B依赖Lib A,如果Lib A使用了预编译,那么会出现这种链接错误:error LNK2011: precompiled object not linked in; image may not run,有两种解决方案:1、让EXE C能接触到Lib A工程,保证EXE C在链接的时候能找到Lib A所有相关的中间产物,这个方案我不能采用,因为对外提供的只是Lib B SDK,不是源码;2、Lib A不使用预编译,这个方案比较方便,不过不使用预编译后会增加编译时间,由于Lib A工程比较小,可以接受。 另外,Lib B如何包含Lib A的问题可以参考:http://www.cnblogs.com/cswuyg/archive/2012/02/03/2336424.html

Singleton、MultiThread、Lib——实现单实例无锁多线程安全API的更多相关文章

  1. 深入浅出单实例Singleton设计模式

    深入浅出单实例Singleton设计模式 陈皓 单实例Singleton设计模式可能是被讨论和使用的最广泛的一个设计模式了,这可能也是面试中问得最多的一个设计模式了.这个设计模式主要目的是想在整个系统 ...

  2. 实现单实例多线程安全API问题

    前阵子写静态lib导出单实例多线程安全API时,出现了CRITICAL_SECTION初始化太晚的问题,之后查看了错误的资料,引导向了错误的理解,以至于今天凌晨看到另一份代码,也不多想的以为singl ...

  3. 无锁,线程安全,延迟加载的单例实现(C#)

    单例(singleton)是非常常见,也非常有用的设计模式,当然了, 面试中也是经常会被问到的:)在几乎所有的项目中都能看到它的身影.简而言之,单例保证了一个自定义类型在整个程序的生命周期只被创建一次 ...

  4. 8.2 GOF设计模式一: 单实例模式 SingleTon

    GOF设计模式一: 单实例模式 SingleTon  整个美国,只有一个“现任美国总统”  比如,在学校,“老师”,有数百个:“校长”,只有一个  系统运行时,如何保证某个类只允许实例化一个对象 ...

  5. 单实例Singleton

    单实例Singleton设计模式可能是被讨论和使用的最广泛的一个设计模式了,这可能也是面试中问得最多的一个设计模式了.这个设计模式主要目的是想在 整个系统中只能出现一个类的实例.这样做当然是有必然的, ...

  6. 设计模式之单实例模式(Singleton)

    原理:将类的构造函数由pubic变为private或者protect,添加获取对象的public 成员函数,返回指向对象的静态指针. 首先来一段简单的代码实现 代码一 class Singleton ...

  7. 单实例redis分布式锁的简单实现

    redis分布式锁的基本功能包括, 同一刻只能有一个人占有锁, 当锁被其他人占用时, 获取者可以等待他人释放锁, 此外锁本身必须能超时自动释放. 直接上java代码, 如下: package com. ...

  8. 无锁编程 - Double-checked Locking

    Double-checked Locking,严格意义上来讲不属于无锁范畴,无论什么时候当临界区中的代码仅仅需要加锁一次,同时当其获取锁的时候必须是线程安全的,此时就可以利用 Double-check ...

  9. 【DPDK】【ring】从DPDK的ring来看无锁队列的实现

    [前言] 队列是众多数据结构中最常见的一种之一.曾经有人和我说过这么一句话,叫做“程序等于数据结构+算法”.因此在设计模块.写代码时,队列常常作为一个很常见的结构出现在模块设计中.DPDK不仅是一个加 ...

随机推荐

  1. windows下使用xShell远程连接virtualbox里面的linux

    第一阶段:基本安装 安装virtual box 在virtualbox里面安装xubuntu:是ubuntu+xfce桌面环境的一个linux的发行版本 在windows下安装Xmanager Ent ...

  2. VS生成事件

    源自:http://www.cnblogs.com/FreeDong/p/3406737.html 如果说磨刀不误砍柴工,同样用好Visual Studio,会大大增加咱.NET程序猿效率.本文说的就 ...

  3. LINQ之路 2:C# 3.0的语言功能(上)

    在上一篇的LINQ介绍中,我们已经看到了隐式类型变量var,扩展方法(extension method)和lambda表达式的身影.没错,他们正是LINQ技术的基石,是他们让LINQ的实现成为可能,并 ...

  4. C,C++,Lisp,Java,Perl,Python

    (译注:圣经记载:在远古的时候,人类都使用一种语言,全世界的人决定一起造一座通天的塔,就是巴别塔,后来被上帝知道了,上帝就让人们使用不同的语言,这个塔就没能造起来. 巴别塔不建自毁,与其说上帝的分化将 ...

  5. 解耦HTML、CSS和JavaScript

    当前在互联网上,任何一个稍微复杂的网站或者应用程序都会包含许多HTML.CSS和JavaScript.随着互联网运用的发展以及我们对它的依赖性日益增加,设定一个关于组织和维护你的前端代码的计划是绝对需 ...

  6. iOS - KVO 键值观察

    1.KVO KVO 是 Key-Value Observing 的简写,是键值观察的意思,属于 runtime 方法.Key Value Observing 顾名思义就是一种 observer 模式用 ...

  7. 从POI到O2O 看百度地图如何走出未来之路

    近期O2O的烧钱融资大战如火如荼,有人已经把O2O大战,用乌合之众的群体心理失控来形容.其实厂商都不傻,O2O烧钱大家都知道,但是大家还知道O2O背后这块大蛋糕价值"万亿级". 有 ...

  8. Java Socket编程----通信是这样炼成的

    Java最初是作为网络编程语言出现的,其对网络提供了高度的支持,使得客户端和服务器的沟通变成了现实,而在网络编程中,使用最多的就是Socket.像大家熟悉的QQ.MSN都使用了Socket相关的技术. ...

  9. CSS3_边框属性之圆角

    一.border-radius是向元素添加圆角边框: border-radius的值不仅能用px单位,你还可以用百分比或者em,但兼容性目前还不太好.(都不能是负值) 1.border-radius ...

  10. /dev/sda3: UNEXPECTED INCONSISTENCY; RUN fsck MANUALLY

    系统强制断电后,出现以下错误: /dev/sda3: UNEXPECTED INCONSISTENCY; RUN fsck MANUALLY 启动系统后在字符界面有两个选项,输入root密码进入维护模 ...