条款16:保证const成员函数的线程安全性

有一个类来表示多项式会非常方便,而在这个类中,其中有一个函数能够计算多项式的根,即那些使得多项式求值结果为零的数值;在计算多项式的根时,我们就把这些根缓存起来,并以返回缓存数值的手法实现roots

class Polynomial {
public:
    using RootsType = std::vector<double>; // 持有值的数据结构,这些数值使得多项式求值结果为零
    RootsType roots() const {
        if (!rootsAreValid) {              // 如果缓存无效
            //...
            rootsAreVali d = true;          // 则计算根,将其存入rootVals
        }
    }

private:
    mutable bool rootsAreValid {false};  // 关于初始化信息
    mutable RootsType rootVals{};        // 条款7
};

从理论上说,roots不会改变它操作的Polynomial对象,然而作为缓存活动组成部分,它可能需要修改rootValsrootsAreValid的数值,这是mutable的经典用例;

设想现在有两个线程同时在同一个Polynomial对象上调用roots

Ploynomial p;
auto rootsOfP = p.roots();     	// 线程1
auto valsGivingZero = p.roots();// 线程2

上述代码在本例中不安全,因为在roots内部,这些线程中一个或者两个可能企图更改数据成员rootsAreaValidrootVals,这就意味着这段代码可能有不同的多个线程在没有同步的情况下读写同一块内存,而这就是数据竞险。解决办法是引入一个mutex

class Polynomial {
public:
    using RootsType = std::vector<double>; // 持有值的数据结构,这些数值使得多项式求值结果为零
    RootsType roots() const {
        std::lock_guard<std::mutex> g(m);  // 加上互斥量
        if (!rootsAreValid) {              // 如果缓存无效
            //...
            rootsAreValid = true;          // 则计算根,将其存入rootVals
        }
    }                                      // 解决互斥量

private:
    mutable std::mutex m;
    mutable bool rootsAreValid {false};  // 关于初始化信息
    mutable RootsType rootVals{};        // 条款7
};

之所以要把std::mutex m声明为mutable,是因为加锁与解锁都不是const成员函数所为;

如果要计算一个成员函数被调用的次数,使用std::atomic类型的计数器将会是一种成本较低的途径

class Point {										// 表示2D点
public:
    double distanceFromOrigin() const noexcept {
        ++callCount;								// 带有原子性的自增操作
        return std::sqrt((x * x) + (y * y));
    }
private:
    mutable std::atomic<unsigned> callCount{0};
    double x, y;
};

如果某类需要缓存计算开销较大的int类型变量,则应该尝试使用一对std::atomic类型的变量来取代互斥量。

class Widget {
public:
    int magicValue() const {
        if (cacheValid) {
            return cacheValue;
        } else {
            auto val1 = expensiveComputation1();
            auto val2 = expensiveComputation2();
            cacheValue = val1 + val2;			// 第一部分
            cacheValid = true;					// 第二部分
            return cacheValue;
        }        
    }

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

然后考虑如下调用:

  • 一个线程调用Widget::magicValue时,观察到cacheValid数值为false,于是执行了两个大开销的计算,并将其和赋值给了cacheValue
  • 与此同时,另一个线程也在调用Widget::magicValue,也观察到cacheValid值为false,于是也执行了第一个线程刚刚完成的两次同样的大开销运算

这种行为与缓存的目标南辕北辙,颠倒对cacheValidcacheValue的赋值顺序可以消除该问题,但是结果却更坏了

class Widget {
public:
    int magicValue() const {
        if (cacheValid) {
            return cacheValue;
        } else {
            auto val1 = expensiveComputation1();
            auto val2 = expensiveComputation2();
            cacheValid = true;					// 第一部分
            return cacheValue = val1 + val2;	// 第二部分
        }        
    }

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

假设cacheValid的数值为false,接着:

  • 一个线程调用Widget::magicValue并执行到了cacheValid值被置为true的时刻
  • 在这一时刻,另一个线程也在调用Widget::magicValue,并检视cacheValid的值。观察到其数值为true后,该线程就把cacheValid的数值返回了,即使此时第一个线程还没有执行对cacheValid的赋值。因此返回值是不正确的。

这里学到的教益是:对于单个要求同步的变量或内存区域,使用std::atomic就够了。但是如果有两个或更多变量或内存区域需要作为一整个单位进行操作,就要使用互斥量了。

class Widget {
public:
    int magicValue() const {
    	std::lock_guard<std::mutex> guard(m);	 // 为m加上互斥量
        if (cacheValid) {
            return cacheValue;
        } else {
            auto val1 = expensiveComputation1();
            auto val2 = expensiveComputation2();
            cacheValid = true;					// 第一部分
            return cacheValue = val1 + val2;	// 第二部分
        }        
    }											// 为m解除互斥量

private:
	mutable std::mutex m;
    mutable std::atomic<bool> cacheValid{false}; // 不再具备原子性
    mutable std::atomic<int>  cacheValue;		 // 不再具备原子性
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值