TSX是新一代Haswell架构上,通过硬件支持的事务性内存(Transactional Memory)解决方案。HLE和RTM都在事务的基础上实现的硬件技术手段。一句话概括Intel的事务性同步扩展(Transactional Synchronization Extension, TSX)的动机:粗粒度锁保证的事务性操作,在高并发下性能下降,作为细粒度锁方案的一种替代,TSX通过硬件辅助保证正确性,编程更友好。
一张有大量数据的表(原讲座中用银行账户记录做比),一种典型的事务性操作,是从其中一个条目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
使用一对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使用两条新的指令标识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问题。
谓原子操作是指,通过一种看起来不可分割的方式来操作内存:线程无法看到原子操作的中间过程。
在现代的处理器上,有很多操作本身就是的原子的。例如,对简单类型的对齐的读和写通常就是原子的。这个依赖平台,有待测试。
1) RMW操作的例子包括:Win32上的_InterlockedIncrement,
iOS
上的
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
发布这些改动。在intel
CMPXCHG指令基础上。
//函数完成的功能是:将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;
}
}
在进行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的访问,相当于有个线程本地变量了)
(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都被操作将被视为冲突。
(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把数据备份出来并且检查冲突这其实就是一个事务。
l 与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