Singleton、MultiThread、Lib——实现单实例多线程安全API遇到的问题

    前阵子写静态lib导出单实例多线程安全API时,出现了CRITICAL_SECTION初始化太晚的问题,之后查看了错误的资料,引导向了错误的理解,以至于今天凌晨看到另一份代码,也不多想的以为singletone double check会出bug,本文做下记录备忘。
    相关知识点:Singleton Double Check、多线程下的局部Static对象、静态Lib中的全局对象。
 
一、singleton double check
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
    这样的代码一般(不考虑全局对象的初始化)没有问题。之前只略看他人的文章,不思考,误以为:g_instance = new SingleInstance ; 这句在线程A的执行会被线程B g_instance == NULL的判断打断,导致线程B返回的g_instance是一个半成品。实际上不会,因为g_instance的赋值是在内存分配、构造函数执行之后做的,而且赋值是原子操作,这没有问题。
    按照文档的说法,g_instance变量应该加上volatile,避免编译器优化,编译器优化之后,可能会导致g_instance变量的赋值在SingleInstance构造函数执行之前。volatile用于表明这个变量是易变的,每一次都直接操作对应内存,而不是用寄存器缓存,不会去优化指令。这里如果不使用它,就可能导致编译器调整汇编指令的顺序,分配完内存就直接把地址赋值给g_instance指针,后面再调用构造函数,它这样调整的理由可能是这样子:分配到的内存指针在后续的执行中没有被修改,先赋值给g_instance和晚赋值给g_instance没有区别。
 
二、导出Lib中慎用全局对象
    我的Lib的导出API提供的数据只需要获取一次就够了,不能多次获取,所以它必须是单实例的、多线程安全的,再考虑到不能浪费频繁的锁消耗,很直接的做法便是用singleton double check。
    首先我选择使用临界区实现锁,而临界区在API被调用之前需要先初始化,于是定义一个Lock封装了临界区的初始化,什么时候初始化?必须是全局对象,如果为定义局部static对象会导致多线程不安全
    static对象不是多线程安全的:Singleton、MultiThread、Lib——实现单实例多线程安全API遇到的问题
Singleton、MultiThread、Lib——实现单实例多线程安全API遇到的问题
    从上图的汇编指令可以看到static对象的构造函数是否被执行的判断逻辑:
1、通过标识值判断是否该执行构造函数(这里的构造函数内联了);
2、执行构造函数,首先把标志值置位。
     有可能多个线程都同时通过了1的判断,导致构造函数被多次执行。
     使用了全局对象之后发现也不可行:导出函数依赖全局对象的初始化,虽然全局对象会在main函数之前初始化,但初始化时机还是可能太晚了,譬如这种情况:lib的使用者也定义了全局对象,并且初始化得更早,使用者的全局对象构造函数里调用了lib的导出函数,导出函数使用了还没初始化的临界区全局对象导致崩溃,更麻烦的是,使用者的dump捕获机制是在main函数里初始化的,生效得太晚,导致dump无法捕获,使这个crash更加隐蔽。C++的全局对象应该尽量少用。exe里面如果使用了全局对象,则需要保证dump捕获机制对所有的代码都生效。
    既然临界区初始化问题无法解决,局部static对象、全局对象都无法使用,需要找到一个不需要初始化又能实现锁的方法:那就是原子操作。
    单纯的原子操作并没有锁的功能,需要配合上:if + Sleep.
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SingleInstance* volatile g_instance;
LONG volatile g_for_lock;
 
SingleInstance* GetInstance()
{
    if (g_instance == NULL)
    {
        LONG pre_value = ::InterlockedExchange(&g_for_lock, 1);
        if (pre_value != 0)
        {
            while(g_instance == NULL)
            {
                ::Sleep(55);
            }
        }
        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非空。问题不逼你,你就不会想到还有这么好的实现思路 :)

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

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

Double Check 相关资料(主要是说java的执行指令调整导致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

Singleton、MultiThread、Lib——实现单实例多线程安全API遇到的问题,布布扣,bubuko.com

Singleton、MultiThread、Lib——实现单实例多线程安全API遇到的问题

上一篇:在Windows 7下配置eCos开发环境的问题和解决方法


下一篇:c# winForm 将窗体状态栏StatusStrip 分成左中右三部分 右边显示当前时间