TSX之有限事务内存,TSX之锁消除技术 和 lock free 编程之比较(有效总结)

TSX(事务同步扩展)

TSX是新一代Haswell架构上,通过硬件支持的事务性内存(Transactional Memory)解决方案。HLE和RTM都在事务的基础上实现的硬件技术手段。一句话概括Intel的事务性同步扩展(Transactional Synchronization Extension, TSX)的动机:粗粒度锁保证的事务性操作,在高并发下性能下降,作为细粒度锁方案的一种替代,TSX通过硬件辅助保证正确性,编程更友好。

TSX overview

适用的场景:

一张有大量数据的表(原讲座中用银行账户记录做比),一种典型的事务性操作,是从其中一个条目a中减去一个数值x,加到条目b中:

[ a = a – x : b = b + x]                                (1)

        如果用一个粗粒度的锁保护整张表的操作,在并发时会碰到如下的问题,比如同时另一个线程对不同的条目c和d操作,原本不冲突的操作,因为粗粒度锁的存在,不得不串行执行。可以想见,高并发下粗粒度锁的方案性能严重下降。

        传统的优化手段是使用细粒度的锁,比如给表中的每一个条目单独加锁,那么上面存在的“假”的数据冲突就可以避免。但是细粒度锁极大增加的设计的复杂度,容易出现难以解决的bug。作为一个例子,考虑在(1)的操作同时,另一个线程进行如下操作:

[ b = b – x : a = a + x]                                (2)

        由于a/b由两个独立的锁保护,完成(1)或(2)的操作,需要获得两把锁,如果(1)(2)不能完整获得两把锁,而是(1)获得lock(a),(2)获得lock(b),即出现死锁。所以细粒度锁的加锁解锁方案需要仔细设计,避免死锁和其他很多问题。

        使用TSX的替代方案:逻辑上TSX是一把粗粒度的锁,将包含事务性的操作的critical section包起来;由硬件自动检测操作中的数据冲突,保证事务性操作的正确性,发掘操作间的并行性,实现上类似每个条目都有细粒度的锁,这被称作lock elision。

 不适用的场景:

       从上面的例子可以看出,TSX主要解决的是粗粒度锁下的“假”数据冲突问题,如果原本不需要细粒度的锁,或者产生冲突的条目少,“真”冲突概率高,那么使用TSX的收益不大。TSX不是银弹。

Q: 我怎么知道什么时候该使用TSX?

A: 如果现在的程序没有性能问题,你可以去休息,喝杯咖啡;如果有我上面场景中的性能问题,你可以试试TSX,用起来也很方便。

性能

典型应用场景下,相对粗粒度锁的方案,TSX的方案在高并发下的性能有明显提升,可以达到接近线性的可扩展性;相对细粒度锁的方案,TSX在高并发下的性能也有小的优势(TSX的开销可能比细粒度锁的开销小)。图不方便贴了。

相比比较火的无锁编程,TSX也有明显的优势。无锁编程不是真的没有锁,而是很强的依赖于原子操作,它的劣势是限制了数据结构(只能用队列等),难于设计和优化,高并发下也有问题。TSX下数据结构的选择更自由(因为使用的是和粗粒度锁一样的临界区的模型),同样的需求用无锁编程难以验证正确性。

底层TM跟踪事务的read-set 和write-set ,以一个64字节的高速缓存行为粒度。这里的read-set 和write- set 分别表明事务在执行过程中已读出或写入的所有高速缓存行。 如果事务的read-set中的一个高速缓存行被另一个线程写入,或者事务的 write-set 中的一个高速缓存行被另一个线程读取或写入,则事务就遇到冲突(conflict)。 对于那些熟悉TM术语的人而言,这就是所谓的强隔离(strong isolation),因为一个非事务性(non-transactional)内存访问可能会导致事务中止(abort)。 冲突通常导致事务中止,且在一个高速缓存行内还可能发生假冲突(falseconflicts)。

TSX可以允许事务相互嵌套,在概念上是通过将嵌套展平成单个事务来处理的。 然而,嵌套的数量有一个具体实现特定的限制,超过此限制将导致中止。 在一个嵌套事务内的任何中止,都将中止所有的嵌套事务。

事务只能使用可高速缓存的回写内存操作(write-back cacheable memory operations),但可以在所有的权限级别下使用。不是所有的x86指令都可安全地用于事务内。 有几个x86指令将导致任何事务(对于HLE或 RTM)中止,特别是CPUID和PAUSE。

此外,在特定的实现中,还有些可能导致中止的指令。 这些指令包括x87和MMX,混合访问XMM和YMM寄存器,EFLAGS寄存器的非状态部分的更新,更新段(segment),调试(debug )或控制(control )寄存器,ring 转换,高速缓存和TLB控制指令,任何非写回(non-writeback)内存类型的访问,处理器状态保存,中断,I / O,虚拟化(VMX),可信执行(SMX)以及一些杂项类型。 注意,这意味着上下文切换(context switch),通常使用状态保存(state saving)指令,几乎总是会中止事务。 一般来说,TSX 实现的目的是要相对于指令向上兼容。 例如,如果一个实现在事务中新增对 VZEROUPPER的支持,则该功能将不会在未来的版本中删除。

也有运行时的行为可能会导致事务中止。大多数的错误和陷阱都会导致中断,而同步例外和异步事件可能会导致中止。自修改代码和访问非写回内存类型也可能导致中止。 

事务的大小也有具体实现特定的限制,且各种微架构的缓冲(buffers)可能是限制因素。 一般情况下,英特尔对有关事务的执行没有提供保证。虽然对于程序员而言这是令人沮丧的,因为它需要一个非事务性的(non-transactional)回退路径,这样就避免了未来的向后兼容性问题。即便当程序员写了一个不太可能成功的事务,它也避免了使系统死锁。

软件

操作系统不需要改变。

主流编译器支持:ICC v13.0以上,GCC v4.8以上,Microsoft VS2012以上。

库:GLIBC的pthread rtm-2.17分支支持。

(我:找到网上有一个C版本的TSX使用例子,http://software.intel.com/en-us/blogs/2012/11/06/exploring-intel-transactional-synchronization-extensions-with-intel-software)

Q: pthread中怎么使用TSX?

A: 只需要动态链接这个版本的pthread库就可以(我:看来pthread使用了TSX重构了一些代码,而不包括TSX的高级封装)。

代码介绍

TSX的模型类似传统的临界区。提供两种编程接口:HLE(HardwareLock Elision)和RTM(Restricted Transactional Memory)。以如下的伪代码为例:

acquire_lock(mutex);

// critical section   

release_lock(mutex)

带锁的代码

传统的基于锁的方案大概是这样的:

       mov eax, 1

Try:   lockxchg mutex, eax

      cmp  eax, 0

      jzSuccess

Spin:  pause

      cmp  mutex, 1

      jz  Spin

      jmp  Try

; critical section …

Release:   mov  mutex, 0

HLE(Hardware Lock Elide/硬件锁消除) code

使用一对compilerhints:xacquire /xrelease。

mov eax, 1

Try:   xacquire xchg mutex, eax

      cmp  eax, 0

       jz Success

Spin:  pause

      cmp  mutex, 1

      jz  Spin

      jmp  Try

; critical section …

Release:   xrelease  mutex, 0

提示:

(1)   两个关键词是hints,在不支持TSX的硬件上直接被忽略。所以兼容性好。

(2)   事务性操作失败(abort)的结果是重新执行传统的有锁代码(legacy code)。

 

RTM(Restricted Transaction Memory/有限事务内存) Code

RTM使用两条新的指令标识critical section:xbegin /xend。

RTM的模型更加灵活:

Retry: xbeginAbort

      cmp  mutex, 0

      jzSuccess

       xabort$0xff

Abort:

       …check%EAX

       …doretry policy

       …

      cmp  mutex, 0

      jnz  Release_lock

       xend

提示:

(1)   事务性操作失败(abort)的后续操作入口由xbegin指定。

(2)   xabort指令通过eax返回一个错误码,用于后续分原因处理。

 

无锁编程

定义:

相关技术:

当你试图满足无锁编程的无阻塞条件时,会出现一系列技术:原子操作、内存屏障、避免ABA问题。

依赖于原子操作

谓原子操作是指,通过一种看起来不可分割的方式来操作内存:线程无法看到原子操作的中间过程。

原子操作一:对简单类型的对齐的读和写

在现代的处理器上,有很多操作本身就是的原子的。例如,对简单类型的对齐的读和写通常就是原子的。这个依赖平台,有待测试。

原子操作二:Read-Modify-Write(RMW)操作

1)        RMW操作的例子包括:Win32上的_InterlockedIncrementiOS上的OSAtomicAdd32以及C++11中的std::atomic<int>::fetch_add。需要注意的是,C++11的原子标准不保证其在每个平台上的实现都是无锁的,因此最好要清楚你的平台和工具链的能力。你可以调用std::atomic<>::is_lock_free来确认一下。不同的CPU系列支持RMW的方式也是不同的。

2)         最常讨论的RMW操作是compare-and-swap(CAS)。在Win32上,CAS是通过如_InterlockedCompareExchange等一系列指令来提供的。通常,程序员会在一个事务中使用Compare-And-Swap循环。这个模式通常包括:复制一个共享的变量至本地变量,做一些特定的工作(改动),再试图使用CAS发布这些改动。在intelCMPXCHG指令基础上。

//函数完成的功能是:将old和ptr指向的内容比较,如果相等,则将new写入到ptr中,返回old,如果不相等,则返回ptr指向的内容。

Linux cmpxchg cmpxchg(void *ptr, unsigned long old, unsigned longnew);

//如果第三个参数与第一个参数指向的值相同,那么用第二个参数取代第一个参数指向的值。函数返回值为原始值。

Windows LONG InterlockedCompareExchange(LPLONGDestination, LONG Exchange, LONG Comperand ); PVOIDInterlockedCompareExchangePointer(PVOID *Destination, PVOIDExchange, PVOID Comperand );

 voidenQ(request, queue)

{

    do{

       local_head = queue->head;

       request->next = queue->head;

        val =cmpxchg(&queue->head, 

       local_head, request);

    }while(val !=local_head)

}

void LockFreeQueue::push(Node* newHead)

{

   for (;;)

    {

       // Copy a shared variable (m_Head) to a local.

       Node* oldHead = m_Head;

 

        // Do some speculative work, not yet visibleto other threads.

       newHead->next = oldHead;

 

       // Next, attempt to publish our changes to the shared variable.

       // If the shared variable hasn't changed, the CAS succeeds and wereturn.

        // Otherwise, repeat.

       if (_InterlockedCompareExchange(&m_Head, newHead, oldHead) ==oldHead)

           return;

    }

}

ABA 问题

在进行CAS操作的时候,因为在更改V之前,CAS主要询问“V的值是否仍然为A”,所以在第一次读取V之后以及对V执行CAS操作之前,如果将值从A改为B,然后再改回A,会使基于CAS的算法混乱。在这种情况下,CAS操作会成功。这类问题称为ABA问题。

对于CAS产生的这个ABA问题,通常的解决方案是采用CAS的一个变种DCAS。
DCAS,是对于每一个V增加一个引用的表示修改次数的标记符。对于每个V,如果引用修改了一次,这个计数器就加1。然后再这个变量需要update的时候,就同时检查变量的值和计数器的值。

要解决ABA问题,就不要重用A。通常通过将标记或版本编号与要进行CAS操作的每个值相关联,并原子地更新值和标记,来处理这类问题。

在像Java或者.net这样的具有垃圾收集机制的环境中,这个问题很简单,只要不循环使用V即可。也就是说,一旦V第一次被使用,就不会再重复使用,如有需要则分配新的V。垃圾收集器可以检查V,保证其不被循环使用,直到当前的访问操作全部结束。(这个做法用垃圾收集器来控制对V的访问,相当于有个线程本地变量了)

三种技术总结

TSX-HLE 特点:

(1)      依然维护锁的概念,但是不真正操作锁,许多线程可以同时获得锁,并对共享数据做无冲突的内存访问:就是锁地地被加到事务内存的read-set, 但是不写入数据到锁地址,线程接着进入事务区域执行,期间HLE事务内部读该锁地址都将返回新数据(译注:该新数据虽然没有写入锁地址,但是被本地硬件保持,在读这个锁地址时返回),用以造成锁已经被获得的假象,实际上并没有做获得锁的操作。但另一个线程的任何读操作都将返回旧数据(真正的锁地址没有被写入数据,锁是unlocked)。

(2)      如果数据冲突,那么丢弃原来事务数据,带锁从新执行一遍:事务执行过程中,如果和其他线程发生数据冲突,处理器体系结构寄存器(architectural registers)的状态将恢复到XACQUIRE之前,并丢弃任何在HLE区域内写入的内存。 线程将再次执行该区域,就像没有HLE,而使用标准的悲观锁行为(pessimistic lockingbehavior)。 

(3)      假锁的数量有具体实现特定的限制。

(4)      尽管嵌套的HLE是存在的,但是递归锁是不被支持的。

(5)      在HLE区域内对锁地址进行写操作,将导致HLE中止。

(6)      冲突检测是64bit宽度的, read-set 和write-set ,以一个64字节的高速缓存行为粒度。譬如int x,y; &x==0x00; &y==0x32;那么事务期间操作x,y都被操作将被视为冲突。

TSX-RTM特点

(1)      不在维护锁的概念,直接在事务中执行代码

(2)      如果数据冲突,那么丢弃原来事务数据到abort代码段,abort的具体实现由程序员决定,可以带锁执行也可以retry一遍RTM. 另外,如果两个事务冲突,都可能被中止。 一个更明智的做法是中止两个事务其中之一。

(3)      支持事务嵌套,估计支持递归事务。

(4)      冲突检测是64bit宽度的。

无锁编程

与spinlock 相比:无锁编程其实是个死循环,但是和spinlock的循环不一样。 Spinlock是围绕着锁,先去拿锁,如果拿不到,那么spin back to re-get 锁。无锁编程是不维护锁的概念,先把数据备份到用户内存中修改数据,如果没有冲突(copy出来的值和原来的值相等)那么就利用原子操作提交数据。与spinlock相比,RMW基础上的CAS在没有冲突的情况下会不涉及到锁,避免大部分的lock/unlock操作,另外CAS是指令级别的原子性,最后一点CAS把数据备份出来并且检查冲突这其实就是一个事务。

与intel TSX相比: 自己维护事务内存(变量本地备份,RWM进行冲突检查);没有锁的概念,CAS原语进行交换赋值;如果有冲突,重新进行一个新的事务,与TSX中的RTM相似,只是事务简单很多;自己维护的事务比较简单,所以存在ABA问题,如要解决必须进行版本管理。

参考文章:

//无锁编程之翻译介绍

http://m.blog.csdn.net/blog/sahusoft/9210029

//事务同步扩展

本节参考: http://blog.csdn.net/linxxx3/article/details/8788153

//事务同步扩展 2

http://www.cnblogs.com/coryxie/archive/2013/02/24/2951244.html

 



你可能感兴趣的:(RTM,无锁编程,HLE,TSX,事务同步扩展)