• 缘起
  • 双buffer “无锁” 设计
  • 指针的切换
    • ptr 竞争条件的解决
    • 指针访问丢失
  • 延伸
  • 结语

缘起

在linux多线程环境下对同一变量进行读写时,经常会遇到读写的原子性问题,即会出现竞争条件。为了解决多个线程对同一变量访问时的竞争条件问题,操作系统层面提供了锁、信号量、条件变量等几种线程同步机制。如果对变量的每次访问都使用上述机制,由于系统调用会陷入内核空间,需要频繁的进行上下文切换,这就导致了程序的时间开销比较大。

自然的,我们就想到,在多线程环境中,在某些情况下是否能减少甚至避免使用系统调用?答案是肯定的。

如果对多线程下的变量访问进行分析,可以看到,线程对变量的访问可以分为以下几类:

  • 一个线程写,另一个线程读,简称一写一读
  • 多个线程写,一个线程读,简称多写一读
  • 一个线程写,多个线程读,简称一写多读。
  • 多个线程写,多个线程读,简称多写多读。

在linux 系统中,多个线程同时读一个变量是不需要同步的,而多个线程同时写一个变量或一个线程写而其他线程读某个变量,是需要同步的,可以总结为:”多读不互斥,而读写和多写互斥“。

由于多个线程对同一变量的读不需要同步,因而一写多读和一写一读并无本质区别,进而可以把多线程下对变量访问依据是否需要同步而合并成如下三类:

  • 一写多读
  • 多写一读
  • 多写多读

解决上面所有的互斥,都可以使用系统调用。上面已经提到,在某些情况下我们是可以避免使用代价高昂的系统调用的。而“一写多读”就是这些特殊情况中的一种。

双buffer “无锁” 设计

使用系统调用进行同步的主要问题在于频繁切换上下文耗时较长,而后台系统的处理速度又是除正确性之外最为关键的指标。为提高系统的运行速度,我们可以使用用其他系统资源来换取时间的办法,从而避免使用锁之类系统调用。在这些方法中,最常见的就是用空间换取时间。

针对一写多读的情况,可以使用”双 buffer“ 及共享指针机制来实现对同一变量高效访问,同时又能保证不会出现竞争条件。这一实现的技术关键点在于以下两个方面:

  • 双 buffer 的备份机制,避免了同时读写同一变量。双buffer 就是指对于通常要被多个线程访问的变量,再额外定义一个备份变量。由于是一写多读,写线程只向备份变量中写入,而所有的读线程只需要访问主变量本身即可。当写进程对备份变量的写操作完成后,会触发主变量指针和备份变量指针的互换操作,即指针切换,从而将原变量和备份变量的身份进行互换,达到数据更新的目的。
  • 共享指针 shared_ptr,由于其记录了对变量的引用次数,因而可以避免指针切换时的“访问丢失”问题。

为了便于理解,本文使用 C++ 中的 map 类型变量作为示意,当然,本文的方法可以推广到一写多读模式下任意数据类型的更新中。使用双 buffer 的示意图如下:

注意ptr 和 bak_ptr 都是整个map 的指针,上面蓝色箭头表示通过两个指针访问 map 中的元素,ptr 和bak_ptr 本身并不指向元素。

在系统启动时,把两个智能指针分别初始化为一个主map 和一个备份 map。之后把全部数据更新到主map中开始对外提供服务。当外部需要读取数据时(多读),全部通过主map 的智能指针 ptr 来实现。而数据的更新全部通过备份map 的指针bak_ptr 来实现。由此可以看出,由于使用了两个map,即双buffer,使得数据的读和写进行了分离,互不影响,不会出现竞争条件,避免了锁的使用。

指针的切换

由于读写分离,双buffer机制下的数据读写不会出现竞争条件。在备份map 中数据更新完成时,必然需要一种方式,使得新数据能被使用到。这里需要做的就是把主map和备份map 的共享指针指向的内容互换,即ptr 和bak_ptr 指向的内容互换。指针切换如下图所示:

那么,在指针互换时,会出现什么问题呢?

在指针的切换过程中,会出现如下两个问题:

  • 由于对主map 的读是多线程的读,会出现多线程同使用主map 共享指针ptr 的情形,而互换指针时,需要对主map 的指针进行写操作,那么对同一指针 ptr 的读和写的竞争条件如何解决?

  • 在准备互换ptr 和 bak_ptr 指向的内容时,如果某个读线程正在使用 ptr 访问主map,直接互换就可能出现读线程再通过ptr获取数据时访问失效的问题,严重的情况下会访问到无效内存导致程序崩溃。这一问题本文简称为”指针访问丢失“问题,类似于常规指针中出现的野指针或悬垂指针的问题。

ptr 竞争条件的解决

当指针切换时,单线程对 bak_ptr 的写操作已经完成,因而对其可以随便读写。但由于多个读线程可能还在使用ptr,切换指针时对 ptr 的读写就要十分的小心。为了避免对 ptr 的读写出现竞争条件,本文使用了自旋锁来对ptr 的读写进行同步。使用自旋锁的原因有两个:

  • 只在指针切换时使用锁,而不是在读写两个map 时使用锁,因而锁的使用频率会非常的低,由此导致的上下文切换的代价是可接受的。
  • 由于指针切换时 ptr 处于的情形是一写多读,指针互换准备对 ptr 进行写操作时,要获取锁的等待时间并不长,并不会有长时间的锁等待出现,因而可以使用代价更小的自旋锁,而不是使用代价更高的读写锁。

指针访问丢失

上面已经介绍了指针访问丢失的情形,即在两个指针切换时,多个读线程可能正在使用ptr。为了避免出现读线程会读取到无效数据,本文使用的方法是利用共享指针的引用计数来实现指针的延迟互换。

解决ptr 的竞争条件和指针访问丢失问题后,就可以安全的使用双buffer 方案了。

最终的代码如下,其中 map_ptr_ 就是主map 指针,bak_ptr_ 是备份map 的指针:

class UpdateData {public:UpdateData():flag_(0) {}void PeriodTask();void SetFlag(int i) {flag_ = i;}private:shared_ptr<map> map_ptr_;SpinLock map_rwspinlock_;shared_ptr<map> bak_map_ptr_;int flag_;shared_ptr<map> GetMainMapPtr(); void SetMainMapPtr(shared_ptr<map> new_map_ptr);void SwitchMapPtr();void PeriodTask();void GetData(shared_ptr<map> ptr) {ptr["abc"] = "def";...}
};// 获取主map 指针
shared_ptr<map> UpdateData::GetMainMapPtr() {Lock(map_rwspinlock_); // 加自旋锁,避免对 ptr 访问出现竞争条件return map_ptr_;  // 主map 指针
}// 设置主map 指针
void UpdateData::SetMainMapPtr(shared_ptr<map> new_map_ptr) {Lock(map_rwspinlock_);  // 加自旋锁,避免对 ptr 访问出现竞争条件map_ptr_ = new_map_ptr;
}// 真正的切换指针
void UpdateData::SwitchMapPtr() {shared_ptr<map> old_map_ptr = GetMainMapPtr();SetMainMapPtr(bak_ptr_);  // 这里新数据已经可以被使用了// 用引用次数来解决访问丢失问题while (!old_map_ptr.unique() {::usleep(10000);  // 指针延迟互换}bak_map_ptr_ = old_map_ptr;bak_map_ptr_->clear();
}// 定时任务
void UpdateData::PeriodTask() {while(flag) {::sleep(300); // 每5分钟更新一次数据GetData(bak_ptr_); // 新数据写到备份 map 中SwitchMapPtr();}
}   

需要注意的是,SwitchMapPtr 中调用 SetMainMapPtr(bak_ptr_) 之后,即使程序一直处在while 循环中,再有新的线程通过 map_ptr_ 来访问主map 的数据时,使用的已经是新的数据了。while 循环是为了解决指针访问丢失问题。当引用次数为1时,即 unique 为真时,表示已经没有读线程再使用旧的 map 了,只剩下SwitchMapPtr 中old_map_ptr 这一个引用了,这时可以安全的释放旧的map,并把它清空当作备份map继续进行数据的更新操作。

从上面可以看出,通过使用双buffer和共享指针,避免了在一写多读模式中对数据的读写频繁加锁,实现了”无锁“ 的设计。

延伸

即然双buffer可以很好的用于一写多读模式,那么对于”多写一读“或”多写多读“模式,是否也可以引入双buffer 模式呢?

在含有多线程写同一变量的情形下下,其实是不太适合使用双buffer 方案的。主要原因是:

  • 多写的情形下,需要在 bak_map 的多个写操作之间通过锁来同步,虽然避免了对读写互斥情形的加锁,但是多线程写时通常对数据的实时性要求较高,如果使用双buffer,所有新数据必须要等到指针切换时才能被使用,很可能达不到实时性要求。
  • 多线程写时若用双buffer,则在指针切换时也需要给bak_map 加锁,并且也要用类似于上面的while 循环来保证没有线程在执行写入操作时才能进行指针切换,而且此时也要等待多读的完成才能进行切换,这时就会出现对 bak_map 的锁定时间过长,在数据更新频繁的情况下是不合适的。

因而,在多写的模式下,还是优先用读写锁等操作系统提供的同步机制。

结语

双buffer 方案在多线程环境下能较好的解决 “一写多读” 时的数据更新问题,特别是适用于数据需要定期更新,且一次更新数据量较大的情形。而这种情形在后台开发中十分常见。

对技术的不断探索与思考往往会有新的收获,我们误闯入的世界,也往往能带给我们不一样的精彩。

我思故我在。


我就是我,疾驰中的企鹅。

我就是我,不一样的焰火。

Linux 多线程 ”一写多读” 模式下的无锁设计相关推荐

  1. WPF MVVM模式下的无阻塞刷新

    MVVM模式下的无阻塞刷新的两种方法: //传统模式下的无刷新调用(主线程开新线程,新线程又调用主线程来更新UI) //第1步先在线程内部计算出需要绑定的数据 //第2步然后再使用Invoke/Beg ...

  2. linux 返回非法指令,linux – ARM Cortex A7在内核模式下返回PMCCNTR = 0,在用户模式下返回非法指令(即使在PMUSERENR = 1之后)...

    我想在Raspberry Pi 2上读取循环计数寄存器(PMCCNTR),它有一个ARM Cortex A7内核.我为它编译了一个内核模块,如下所示: #include #include int in ...

  3. linux efi 双系统,EFI+GPT模式下Linux与Windows双系统要诀

    本文并非要对 BIOS/EFI/MBR/GPT 等进行理论探讨,相关知识请各位自行搜索学习.本着薄荷网一贯坚持的实操原则,本文主要是介绍在"EFI引导+GPT分区"模式下,安装 L ...

  4. Linux多线程开发-线程同步-读写锁pthread_rwlock_t

    1.读写锁概念 对资源的访问抽象为两种类型,即独占和共享.独占资源具有排他性,例如写操作.共享资源可以同时由多个线程访问,不对该资源执行写操作.针对资源的读和写操作分别加锁,写操作加锁与互斥锁相同,但 ...

  5. linux电子词典项目流程,毕业设计_linux下电子词典的设计与实现.doc

    毕业设计_linux下电子词典的设计与实现 毕业设计(论文) 题目:Linux下电子词典的设计与实现 学 生 姓 名 学 号 201022080223 班 级 通信102202H班 所属院(系) 电子 ...

  6. UNIX(多线程):28---双buffer “无锁” 设计

    在linux多线程环境下对同一变量进行读写时,经常会遇到读写的原子性问题,即会出现竞争条件.为了解决多个线程对同一变量访问时的竞争条件问题,操作系统层面提供了锁.信号量.条件变量等几种线程同步机制.如 ...

  7. Linux多线程实践(6) --Posix读写锁解决读者写者问题

    Posix读写锁 int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restr ...

  8. linux vi模式替换,linux基础命令之:vi模式下查找和替换

    一.查找 查找命令 /pattern :向下查找pattern匹配字符串 ?pattern:向上查找pattern匹配字符串 使用了查找命令之后,使用如下两个键快速查找: n:按照同一方向继续查找 N ...

  9. 用uefi安装linux系统安装win7系统分区,UEFI模式下Win/Linux双系统安装

    自从Linux阵营的Ubuntu异军突起之后,双系统的安装一直是简单友好的.先装Windows再装Linux,只要新分区(挂载点 Mount point)的选择没出问题,多系统的Grub启动菜单就会在 ...

最新文章

  1. c#switch语句判断成绩_Go的条件判断语句
  2. (转)android 在电脑上显示真机屏幕
  3. .so 依赖目录 cmake_CMake 的研究与学习笔记
  4. [BAT][JAVA]定时任务之-Quartz使用篇(通过这个配置可以知道在做Quartz的时候需要的jar文件/Cron表达式使用语法/常用Cron表达式)
  5. roads 构筑极致用户体验_长安马自达「悦马星空」计划上线,为用户带来极致服务体验...
  6. 图结构练习——最小生成树
  7. java script创建对象_JavaScript七种非常经典的创建对象方式
  8. Python-DDoS攻击
  9. 当一盆植物在MIT成了精,不,它只是成了机器人
  10. 一分钟搞懂的算法之BPE算法
  11. delphi2007安装说明(备忘)
  12. Android Studio查看Android源码
  13. android 自定义图片裁剪,Android自定义View实现照片裁剪框与照片裁剪功能
  14. 谷歌小恐龙PHP代码,Chrome小恐龙前端修改代码代码总结
  15. 陶哲轩实分析 3.4
  16. python的对文档密码的简单破解
  17. 智和网管平台SugarNMS政府部门综合网管
  18. Roy Li:币市里,我相信社区的力量和草根的崛起
  19. 安卓手机卡顿怎么解决_苹果七系统内存满了手机卡顿解决方法
  20. Xshell6下载与安装

热门文章

  1. 思科意欲收购 Banzai Cloud,此举将成为思科在云原生领域的第二笔重要收购
  2. 面对Spring Boot 3最低支持Java17如洪水猛兽袭来,何去何从
  3. 这7个翻译神器超级猛!网页翻译、文档翻译,无所不能
  4. 拼多多和猿辅导SQL题
  5. 曾经的京东001号员工,跟了刘强东18年,现在他的境况到底如何?
  6. 轻JS逆向分析“攒经验”项目之某交易所Sign加密参数逆向分析
  7. keras神经风格迁移_深度神经风格迁移
  8. 字节流和字符流的介绍和使用
  9. QTextStream格式带中文对齐输出问题
  10. java连接dataworks_DataWorks2.0的“业务流程”与1.0的“工作流”的对比