菜鸟笔记
提升您的技术认知

用c 写一个单例模式-ag真人游戏

“请用c 写一个单例,考虑一下多线程环境。”
这是一个常见的面试题,别人问过我,我也问过别人。
这个问题可以很简单,也可以很复杂。

简单有效的单例

class singleton {
public:
    static singleton* getinstance() {
        singleton singleton;
        return &singleton;
    }
};

在c 11中静态局部变量的初始化是线程安全的,。
这种写法既简单,又是线程安全的,可以满足大多数场景的需求。

饿汉模式

单例在程序初期进行初始化。即如论如何都会初始化。

class singleton {
public:
    static singleton* getinstance() {
        return singleton;
    }   
static singleton* singleton;
};
singleton* singleton::singleton = new singleton();

这种写法也是线程安全的,不过singleton的构造函数在main函数之前执行,有些场景下是不允许这么做的。改进一下:

class singleton {
public:
    static singleton* getinstance() {
        return singleton;
    }   
    int init();
    static singleton* singleton;
};
singleton* singleton::singleton = new singleton();

将复杂的初始化操作放在init函数中,在主线程中调用。

懒汉模式

单例在首次调用时进行初始化。

class singleton {
public:
    static singleton* getinstance() {
        if (singleton == null) {
            singleton = new singleton();
        }
        return singleton;
    }
    static singleton* singleton;
};
singleton* singleton::singleton = null;

这样写不是线程安全的。改进一下:

class singleton {
public:
    static singleton* getinstance() {
        lock();
        if (singleton == null) {
            singleton = new singleton();
        }
        unlock();
        return singleton;
    }
    static singleton* singleton;
};
singleton* singleton::singleton = null;

这样写虽是线程安全的,但每次都要加锁会影响性能。

dclp(double-checked locking pattern)

在懒汉模式的基础上再改进一下:

class singleton {
public:
    static singleton* getinstance() {
        if (singleton == null) {
            lock();
            if (singleton == null) {
                singleton = new singleton();
            }
            unlock();
        }
        return singleton;
    }
    static singleton* singleton;
};
singleton* singleton::singleton = null;

两次if判断避免了每次都要加锁。但是,这样仍是不安全的。因为”singleton = new singleton();”这句不是原子的。
这句可以分为3步:

  1. 申请内存
  2. 调用构造函数
  3. 将内存指针赋值给singleton

上面这个顺序是我们期望的,可以编译器并不会保证这个执行顺序。所以也有可能是按下面这个顺序执行的:

  1. 申请内存
  2. 将内存指针赋值给singleton
  3. 调用构造函数

这样就会导致其他线程可能获取到未构造好的单例指针。
解决办法:

class singleton {
public:
    static singleton* getinstance() {
        if (singleton == null) {
            lock();
            if (singleton == null) {
                singleton* tmp = new singleton();
                memory_barrier();  // 内存屏障
                singleton = tmp;
            }
            unlock();
        }
        return singleton;
    }
    static singleton* singleton;
};
singleton* singleton::singleton = null;

语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。简单的说就是保证指令一定程度上的按顺序执行,避免上述所说的乱序行为。
把单例写成这么复杂也是醉了。

返回指针还是引用?

singleton返回的实例的生存期是由singleton本身所决定的,而不是用户代码。我们知道,指针和引用在语法上的最大区别就是指针可以为null,并可以通过delete运算符删除指针所指的实例,而引用则不可以。由该语法区别引申出的语义区别之一就是这些实例的生存期意义:通过引用所返回的实例,生存期由非用户代码管理,而通过指针返回的实例,其可能在某个时间点没有被创建,或是可以被删除的。但是这两条singleton都不满足,所以返回引用更好一些。

结论

class singleton {
public:
    static singleton& getinstance() {
        static singleton singleton;
        return singleton;
    }
    // 如果需要有比较重的初始化操作,则在安全的情况下初始化
    int init();
private:
    // 禁用构造函数、拷贝构造函数、拷贝函数
    singleton();
    singleton(const singleton&);
    singleton& operator=(const singleton&);
};

这种写法比较简单,可以满足大多数场景的需求。如果不能满足需求,再考虑dclp那种复杂的模式。如《unix编程艺术》中所说:“keep it sample, stupid!”

网站地图