C++Item 16: 让const成员函数做到线程安全

正文翻译自modern effective
C++,由于水平有限,故不可能保证翻译完全正确,欢迎指出错误。谢谢!

博客已经搬到这里啦

即使我们以数学领域里干活,大家恐怕会见发现用一个近乎来代表多项式会老有利。在是类似中,假诺起一个函数能总括多选式的根(也就是,多项式等于0时,各样未知量的价)将转移得生有益于。这多少个函数不碰面改变多项式,所以老自然就想开将她注脚也const:

class Polynomial{
public:
    using RootsType =               //一个存放多项式的根的数据结构
        std::vector<double>;        //(using的信息请看Item 9)
    ...

    RootsType roots() const;

    ...

};

计量多项式的根代价可能大高,所以若不用统计的话,大家固然非想念总计。假如我们得使算,那么我们终将不想念多次乘除。因而,当大家必须使算的当儿,大家拿计后底大半项式的根缓存起来,并且于roots函数重回缓存的到底。这里给有极主题的点子:

class Polynomial{
public:
    using RootsType = std::vector<double>;

    RottsType roots() const
    {
        if(!rootsAreValid){         //如果缓存不可用

            ...                     //计算根,把它们存在rootVals中

            rootsAreValid = true;
        }

        return rootVals;
    }

private:
    mutable bool rootsAreValid{ false };    //初始化的信息看Item 7
    mutable RootsType rootVals{};
};

概念上来说,roots的操作不碰面转Polynomial对象,可是,对于她的苏醒存行为的话,它恐怕用修改rootVals和rootsAreValid。这就是mutable很经典的用意况,这也尽管是干吗这些分子变量的宣示带有mutable。

今昔想象一下闹些许只线程同时调用同一个Polynomial对象的roots:

Polynomuial p;

...


/*-------- 线程1 -------- */      /*-------- 线程2 -------- */

auto rootsOfP = p.roots();          auto valsGivingZero = p.roots();

客户代码是截然创造之,roots是const成员函数,这即表示,它表示一个诵读操作。在差不多线程中非一并地履一个念操作是安之。至少客户是这般虽然的。可是在这种情形下,却休是这么,因为当roots中,这半独线程中之一个或零星单还或尝试去窜成员变量rootsAreValid和rootVals。这代表立刻段代码在一贯不联手的情事下,六个不等的线程读写及同截内存,这实在即使是data
race的定义。所以就段代码会时有暴发免定义之行。

近年来之问题是roots被声称也const,不过其却非是线程安全之。这样的const注解在C++11以及C++98中依然天经地义的(取多项式的一干二净不会面改变多项式的值),所以我们得还凑巧的地方是线程安全之少。

缓解之题目最简易的办法就是绝常用的点子:使用一个mutex:

class Polynomial{
public:
    using RootsType = std::vector<double>;

    RottsType roots() const
    {
        std::lock_guard<std::mutex> g(m);       //锁上互斥锁
        if(!rootsAreValid){                     //如果缓存不可用

            ...                                 

            rootsAreValid = true;
        }

        return rootVals;
    }                                           //解开互斥锁

private:
    mutable std::mutex m;
    mutable bool rootsAreValid{ false };    
    mutable RootsType rootVals{};
};

std::mutex
m被声称也mutable,因为对它们加锁和解锁调用的且不是const成员函数,在roots(一个const成员函数)中,要是非这么讲明,m将被视为const对象。

值得注意的凡,因为std::mutex是一个move-only类型(也就是是,那么些类此外靶子只可以move无法copy),所以把m添加到Polynomial中,会给Polynomial失去copy的力量,可是它们要会于move的。

当一部分状态下,一个mutex是负担过重的。举个例子,假设您想做的工作就是总括一个分子函数被调用了有点坏,一个std::atomic计数器(也不怕是,另外的线程保证看正在它的(counter的)操作不停顿地召开停止,看Item
40)平日是上那么些目标的再度廉价的方。(事实上是匪是更廉价,看重让公跑代码的硬件和标准库中mutex的贯彻)这里叫起怎么动std::atomic来总结调用次数的例证:

class Point {
public:
    ...

    double distanceFromOrigin() const noexcept      //noexcept的信息请看Item 14
    {
        ++callCount;                                //原子操作的自增

        return std::sqrt((x * x) + (y * y));
    }

private:
    mutable std::atomic<unsigned> callCount{ 0 };
};

同std::mutex相似,std::atomic也是move-only类型,所以出于callCount的在,Point也是move-only的。

因为比起mutex的加锁和解锁,对std::atomic变量的操作时更廉价,所以若可能碰面超负荷倾向于std::atomic。举个例子,在一个接近吃,缓存一个“统计昂贵”的int,你或许会晤尝试用相同针对std::atomic变量来代表一个mutex:

class Widget {
public:
    ...

    int magicValue() const
    {
        if (cacheValid) return cachedValue;
        else{
            auto val1 = expensiveComputation1();
            auto val2 = expensiveComputation2();
            cachedValue = val1 + val2;              //恩,第一部分
            cacheValid = true;                      //恩,第二部分
            return cachedValue;
        }
    }

private:
    mutable std::atomic<bool> cacheValid { false };
    mutable std::atomic<int> cachedValue;
};

这可以干活,但是有时它碰面工作得死去活来艰巨,考虑一下:

  • 一个线程调用Widget::magicValue,看到cacheValid是false的,执行了一定量只昂贵之精打细算,并且把其的和赋给cachedValue。
  • 于此时间点,第二个线程调用Widget::magicValue,也视cacheValid是false的,因而等同举办了贵之精打细算(这些算第一独线程已经做到了)。(这个“第二单线程”事实上可能是一致名目繁多线程,也尽管汇合不断地展开及时昂贵的揣测)

诸如此类的所作所为与大家采纳缓存的目标是并行背弃的。换一下cachedValue和CacheValid赋值的次第可以破这问题(不断举行再总括),可是错的愈发离谱了:

class Widget {
public:
    ...

    int magicValue() const
    {
        if (cacheValid) return cachedValue;
        else{
            auto val1 = expensiveComputation1();
            auto val2 = expensiveComputation2();
            cacheValid = true;                      //恩,第一部分                
            return cachedValue = val1 + val2;       //恩,第二部分    
        }
    }
    ...
};

想象一下cacheValid是false的场地:

  • 一个线程调用Widget::magicValue,并且刚刚实施了:把cacheValid设置也true。
  • 并且,第二只线程调用Widget::magicValue,然后检查cacheValid,发现其是true,然后,就算第一个线程还尚未把总括结果缓存下来,它要一贯归cachedValue。因而,重临的价是勿得法的。

被咱们吸取教训。对于单一的变量或者内存单元,它们要一块时,使用std::atomic就够用了,不过一旦您用处理多少个或再一次多的变量或内存单元,并把她就是一个一体化,那么你即便应有下mutex。对于Widget::magicValue,看起应当是这样的:

class Widget {
public:
    ...

    int magicValue() const
    {
        std::lock_guard<std::mutex> guard(m);       //锁住m
        if (cacheValid) return cachedValue;
        else{
            auto val1 = expensiveComputation1();
            auto val2 = expensiveComputation2();
            cachedValue = val1 + val2;              
            cacheValid = true;                      
            return cachedValue;
        }
    }                                               //解锁m
    ...

private:
    mutable std::mutex m;
    mutable int cachedValue;                    //不再是atomic了
    mutable bool cacheValid { false };

};

现行,这些Item是基于“多线程可能又施行一个靶的const成员函数”的如。假设你要描写一个const成员函数,并且你能确保这里没多于一个底线程会执行这目的的cosnt成员函数,那么函数的线程安全就是不紧要了。举个例子,即便一个近乎的成员函数只是计划被单线程使用的,那么这成员函数是未是线程安全就是非重要了。在这种场合下,你会制止mutex和std::atomic造成的负。以及免受“包含它们的器皿将成为move-only”的熏陶。然则,这样的任性线程(threading-free)变得更为不日常表现了,它们还将更换得更加难得。未来,const成员函数的多线程执行得会变成主题,这即便是胡你用确保您的const成员函数是线程安全的。

            你如牢记的从事
  • 叫const成员函数做到线程安全,除非你管它们永远不相会就此在多线程的环境下。
  • 比由mutex,使用std::atomic变量能提供再好的特性,可是她只抱处理单一的变量或内存单元