无锁数据结构一

lock-free

本文对lock-free进行介绍,主要介绍原子性操作,下一篇介绍内存访问控制

什么是lock-free

What Is It

Lock-Free Programming Techniques

无锁数据结构

无锁数据结构的实现主要基于两个方面:原子性操作和内存访问控制方法。下面先记录原子性操作相关知识。

原子性操作

原子性操作可以简单地分为读写(read and write)、原子性交换操作(read-modify-write,RMW)两部分。原子操作可认为是一个不可分的操作;要么发生,要么没发生,我们看不到任何执行的中间过程,不存在部分结果(partial effects),就像事务。

在现代处理器中,简单的数据类型的对齐读写操作一般是原子的,而RMW更进一步的,允许进行更复杂的原子事务性操作。

下面以x86架构为例介绍其如何实现原子性操作,其通过三种机制保证原子性:(详情可参考12)

  1. 一些基本的内存读写操作已由硬件保证了其原子性。如单字节读写,奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的。
  2. 通过对总线加锁来保证。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。
  3. 通过对缓存加锁来保证。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。

在不同平台RMW操作有不同实现,如:

  • _InterlockedIncrement _InterlockedCompareExchange(Win32);
  • OSAtomicAdd32(iOS);
  • std::atomic<>::fetch_add``std::atomic<>::compare_exchange_strong (c++11) .

在构建无锁数据结构时需要用到RMW操作,其包括:compare-and-swap (CAS)、fetch-and-add (FAA)、test-and-set (TAS) 等等。其中最基本的是CAS,其他操作可通过CAS实现。

CAS

CAS伪代码如下:

bool CAS( int * pAddr, int nExpected, int nNew )
atomically {
    if ( *pAddr == nExpected ) {
         *pAddr = nNew ;
         return true ;
    }
    else
        return false ;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

但在实际中往往会想要知道,失败后,pAddr的当前值是多少,故可修改如下。

int CAS( int * pAddr, int nExpected, int nNew )
atomically {
      if ( *pAddr == nExpected ) {
           *pAddr = nNew ;
           return nExpected ;
       }
       else
            return *pAddr
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

c++11中CAS为 
std::atomic::compare_exchange_weak
std::atomic::compare_exchange_strong
其如同用std::memcmp 原子地比较 *this 的对象表示和 expected 的对象表示,而若它们逐位相等,则以 desired 替换前者(进行读修改写操作)。否则,将 *this 中的实际值加载进 expected (进行加载操作)。如同用 std::memcpy 进行复制。详情可查看cppreference

ABA问题

ABA问题是所以基于CAS基本类型的无锁容器的一个巨大灾难.CAS算法实现的一个重要前提是需要取出内存中某时刻的数据,而在下一时刻比较并替换,那么就存在了一个时间差,这个时间差会内数据有可能变化。

考虑一个栈数据顺序为 ABC,线程1pop,执行到取出A值,但还没CAS,此时要替换成的数据为A->next即B线程2pop了A、B,然后push A 
,顺序变为AC,然后线程1继续执行CAS,比较时栈顶仍为A比较成功,替换成B(已不在栈中),栈数据出错。

其解决方法有:标签指针(Tagged pointers)、险象指针(Hazard pointer)等。在其他文章介绍。

对于实现了load-linked、store-conditional (LL/SC) 这样的操作对的处理器,其不会发生ABA问题。

其伪代码如下:

word LL( word * pAddr ) {
      return *pAddr ;
}

bool SC( word * pAddr, word New ) {
      if ( data in pAddr has not been changed since the LL call) {
           *pAddr = New ;
           return true ;
      }
      else
         return false ;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

LL/SC对以括号运算符的形式运行,Load-linked(LL) 运算仅仅返回 pAddr 地址的当前变量值。如果 pAddr 中的数据在读取之后没有变化,那么 Store-conditional(SC) )操作会将LL读取 pAddr 地址的数据存储起来。这种变化之下,任何 pAddr 引用的缓存行修改都是明确无误的。为了实现 LL/SC 对,程序员不得不更改缓存结构。简而言之,每个缓存行必须含有额外的比特状态值(status bit)。一旦LL执行读运算,就会关联此比特值。任何的缓存行一旦有写入,此比特值就会被重置;在存储之前,SC操作会检查此比特值是否针对特定的缓存行。如果比特值为1,意味着缓存行没有任何改变,pAddr 地址中的值会变更为新值,SC操作成功。否则本操作就会失败,pAddr 地址中的值不会变更为新值。

CAS通过LL/SC对得以实现,具体如下:

bool CAS( word * pAddr, word nExpected, word nNew ) {
   if ( LL( pAddr ) == nExpected )
      return SC( pAddr, nNew ) ;
   return false ;
}
  • 1
  • 2
  • 3
  • 4
  • 5

但LL/SC由于伪共享(False sharing)也存在问题,简单来说即是多个共享变量使用同一缓存行,只要有一个变量改变,即认为缓存行数据无效(SC失败),详情可参考4。

NOTE

C++11的原子标准并不保证其在所以平台的实现都是lock-free的(std::atomic_flag 除外),可通过std::atomic<>::is_lock_free确认

All atomic types except for std::atomic_flag may be implemented using mutexes or other locking operations, rather than using the lock-free atomic CPU instructions. Atomic types are also allowed to be sometimes lock-free, e.g. if only aligned memory accesses are naturally atomic on a given architecture, misaligned objects of the same type have to use locks.

参考资料

  1. 多线程程序中操作的原子性
  2. 原子操作的实现原理
  3. An Introduction to Lock-Free Programming
  4. 无锁数据结构(基础篇):原子性、原子性原语

猜你喜欢

转载自blog.csdn.net/hellozhxy/article/details/80820257
今日推荐