缘起
在linux多线程环境下对同一变量进行读写时,经常会遇到读写的原子性问题,即会出现竞争条件。为了解决多个线程对同一变量访问时的竞争条件问题,操作系统层面提供了锁、信号量、条件变量等几种线程同步机制。如果对变量的每次访问都使用上述机制,由于系统调用会陷入内核空间,需要频繁的进行上下文切换,这就导致了程序的时间开销比较大。
自然的,我们就想到,在多线程环境中,在某些情况下是否能减少甚至避免使用系统调用?答案是肯定的。
如果对多线程下的变量访问进行分析,可以看到,线程对变量的访问可以分为以下几类:
- 一个线程写,另一个线程读,简称一写一读
- 多个线程写,一个线程读,简称多写一读
- 一个线程写,多个线程读,简称一写多读。
- 多个线程写,多个线程读,简称多写多读。
在linux 系统中,多个线程同时读一个变量是不需要同步的,而多个线程同时写一个变量或一个线程写而其他线程读某个变量,是需要同步的,可以总结为:”多读不互斥,而读写和多写互斥“。
由于多个线程对同一变量的读不需要同步,因而一写多读和一写一读并无本质区别,进而可以把多线程下对变量访问依据是否需要同步而合并成如下三类:
- 一写多读
- 多写一读
- 多写多读
解决上面所有的互斥,都可以使用系统调用。上面已经提到,在某些情况下我们是可以避免使用代价高昂的系统调用的。而“一写多读”就是这些特殊情况中的一种。
双buffer “无锁” 设计
使用系统调用进行同步的主要问题在于频繁切换上下文耗时较长,而后台系统的处理速度又是除正确性之外最为关键的指标。为提高系统的运行速度,我们可以使用用其他系统资源来换取时间的办法,从而避免使用锁之类系统调用。在这些方法中,最常见的就是用空间换取时间。
针对一写多读的情况,可以使用”双 buffer“ 及共享指针机制来实现对同一变量高效访问,同时又能保证不会出现竞争条件。这一实现的技术关键点在于以下两个方面:
- 双 buffer 的备份机制,避免了同时读写同一变量。双buffer 就是指对于通常要被多个线程访问的变量,再额外定义一个备份变量。由于是一写多读,写线程只向备份变量中写入,而所有的读线程只需要访问主变量本身即可。当写进程对备份变量的写操作完成后,会触发主变量指针和备份变量指针的互换操作,即指针切换,从而将原变量和备份变量的身份进行互换,达到数据更新的目的。
- 共享指针 shared_ptr,由于其记录了对变量的引用次数,因而可以避免指针切换时的“访问丢失”问题。
为了便于理解,本文使用 C++ 中的 map 类型变量作为示意,当然,本文的方法可以推广到一写多读模式下任意数据类型的更新中。使用双 buffer 的示意图如下: