前言:
做过GUI开发的同学, 都知晓双缓存机制. 其过程为先把所有的场景和实体对象画到一个备份canvas, 然后再把备份canvas的内容整个填充真正的画板canvas中. 如果不采用双缓存机制, 你的画面有可能会出现闪烁和抖动.
究其原因是整个绘制过程, 包含清屏, 绘制场景和各个实体. 其耗时远远大于单个canvas的复制. 进而导致CPU写canvas的速率小于LCD读取canvas的速率. 这样就出现闪烁的现象了.
在后台服务中, 也会遇到类似的情形: 当数据/资源需要更新时, 采用直接增量更新的方式代价大(耗时长, 阻塞服务可用/实时响应), 由此引入back buffer,做0/1切换.
本文以"配置文件热载更新"为例, 着重介绍0/1切换的思路和优化技巧.
热载更新:
以往更新配置时, 往往需要重启服务进程. 为了提高服务的可用性, 更方便运维.
采取的改进方式是:
1) 引入配置中心服务(ConfigServer)
把模块的配置文件搁置在ConfigServer中, 具体模块从ConfigServer中获取(拉起/通知).
2) 监控本地配置文件变更
进程模块通过定期轮询/事件触发的方式, 感知配置文件是否发生变化, 若发生变化, 则重新载入.
但无论采用何种方式, 势必存在切换过程.
切换特点:
把切换的双方定义为前端和后端, 前端资源往往被N个线程访问(静态只读), 后端资源往往是一个线程更新写. 于是就形成了一个N读1写的格局.
具体在c/c++实现时, 切换过程往往就是一个指针的重新赋值, 十分简单.
但问题也就隐藏在这了, 在切换后的旧资源销毁过程中, 存在多线程的竞态冲突风险.
有人可能会提议, 如果对资源的访问和资源的切换加相同的锁保护, 就没有这个问题. 但在低频率切换的场景下, 加锁带来的性能损失, 有些得不偿失.
无锁0/1切换:
是否存在无锁的切换方式呢?
1). 延迟销毁
工作线程持有并访问旧资源句柄时间不长, 可以设定一个时间窗口, 该时间窗口内属于保护期, 禁止对旧资源进行销毁.
注: 在绝大多数场景下, 该方案满足条件. 只是理论上, 不排除低概率事件.
2). 带引用计数的智能指针切换
我们借助boost的shared_ptr来构建切换的小例子
#include <boost/shared_ptr.hpp>#include <stdint.h>
#include <stdio.h>class Config {
};class DataCenter {
public:DataCenter() {}void init() {active_idx = 0;switchover[active_idx].reset(new Config());}// *) 切换函数, 由更新线程调用void swith() {uint32_t unactive_idx = (active_idx == 0) ? 1 : 0;uint32_t old_active_idx = active_idx;// *) 新资源ready switchover[unactive_idx].reset(new Config());// *) 正式切换active_idx = unactive_idx;// *) 旧资源reset, 引入计数减一switchover[old_active_idx].reset();}// *) 访问资源, 由前端线程调用boost::shared_ptr<Config> getConfig() {return switchover[active_idx];}private:volatile uint32_t active_idx;// *) 切换数组boost::shared_ptr<Config> switchover[2];
};
巧用boost::shared_ptr内部有个原子计数器和代理指针, 借助RAII的思想完美的实现了无引用时的自动清理工作. 也避免了上述的竞态冲突.
总结:
在服务模块中的0/1切换有很多, 这边简述了下解决方案, 没有细致展开, 权当个人的学习笔记.
写在最后:
如果你觉得这篇文章对你有帮助, 请小小打赏下. 其实我想试试, 看看写博客能否给自己带来一点小小的收益. 无论多少, 都是对楼主一种由衷的肯定.