Skip to content

门泊吴船亦已谋

并发环境下实现懒加载

懒加载是指在用到某个变量时才进行初始化
然后并发环境下懒加载可能会有访问冲突、重复初始化等问题,很容易爆炸
因此需要一些操作

单线程懒加载

一般这样写

class Test
{
    shared_ptr<string> str;

public:
    shared_ptr<string> GetStr()
    {
        if (!str)
            str = make_shared<string>("Test str");
        return str;
    }
};

然后发现一个问题是如果 str 没有初始化,此时有多个线程同时访问 GetStr 方法,它们可能会同时把这个 str 初始化多次。虽然这里用的是 string 类型,问题不大,但如果是其他类型就很可能会爆炸

简单加锁

解决并发访问问题,可以加个锁

class Test
{
    shared_ptr<string> str;
    mutex mtx;

public:
    shared_ptr<string> GetStr()
    {
        lock_guard<mutex> lk(mtx);
        if (!str)
            str = make_shared<string>("Test str");
        return str;
    }
};

然而这样写也有问题,假设对于 str 的访问(除了初始化)都是只读的,那么在 str 初始化之后其实各个线程访问 str 都是不会产生竞争条件的,根本不需要上锁,因此这样写会造成巨大的性能浪费

Double check

简单地说就是在上锁之前检查一下是否已经初始化,如果已经初始化了,就不用上锁。其实很简单,就是写起来不怎么高雅

为什么叫做 Double check 呢,因为上锁之后还要检查一下初始化情况,所以总共检查两次

class Test
{
    shared_ptr<string> str;
    mutex mtx;

public:
    shared_ptr<string> GetStr()
    {
        if (!str)
        {
            lock_guard<mutex> lk(mtx);
            if (!str)
                str = make_shared<string>("Test str");
        }
        return str;
    }
};

然而这样写还是有风险。在某一个线程拿到了锁,初始化到一半的时候,也就是 str 的指针非空且分配了内存但是它内部数据没有初始化;而此时另一个线程却可以直接拿到 str,且 str 内部的数据是爆炸的,就会产生未定义行为。这种情况的原因可能是编译器指令重排或者处理器乱序执行

std::call_once

C++钦定的只执行一次,看代码就行了

class Test
{
    shared_ptr<string> str;
    once_flag flag;

    void InitStr() {
        str = make_shared<string>("Test str");
    }

public:
    shared_ptr<string> GetStr()
    {
        call_once(flag, &Test::InitStr, this);
        return str;
    }
};

局部静态变量

如果需要懒加载的对象是一个单例,可以考虑使用局部静态变量

局部静态变量只有在被用到的时候才会分配内存并初始化。当然看起来还是存在竞争条件,但是在 C++11 标准中,规定了静态局部变量要线程安全,所以还是可以放心食用

class Test
{
public:
    shared_ptr<string> GetStr()
    {
        static shared_ptr<string> str = make_shared<string>("Test_str");
        return str;
    }
};

需要注意的是,静态成员变量、全局变量和文件域的静态变量都是在程序运行开始就分配内存并初始化好了,因此它们的初始化过程也是线程安全的,不过这样就不是懒加载了