STM Introduction

来源:互联网 发布:绝密曼哈顿计划 知乎 编辑:程序博客网 时间:2024/04/25 20:12

STM简介

 

译自 维基百科(http://en.wikipedia.org/wiki/Software_transactional_memory)

 

    简介:STM(software transactional memory)是一种类似于数据库事务处理的并发控制机制,用于控制并行计算中对共享变量的访问。它取代了传统的基于锁同步的并发控制方法。一个transaction就是一段包含一系列对共享变量的读和写操作的代码,而且这些操作在逻辑上必须即时完成,亦即中间执行状态对于其他transaction来说是不可见的,本质上,在一个transaction执行过程中,不允许其他线程的interleaving。HTM的思想在1986年由Tom Knight首次提出,之后由 Maurice Herlihy和J.Eliot B.Moss将其发扬光大,在1995年,Nir Shavit和Dan Touitou将HTM的思想扩展到了软件层次,提出了STM。之后STM逐渐成为学术界研究的重点,并且其实用化进程也在加快。

    1 性能

    与使用lock相比,使用STM更为简介:单个线程对共享变量的修改无需考虑其他线程的操作,它将一个transaction中的每次读或者写都记录在log中,之后将transaction中所作的修改提交,如果提交成功,则对共享变量的修改生效,如果提交失败,提交失败是因为当前transaction对共享变量的修改与其他线程的修改相冲突。提交失败后,所有操作回滚,从头再执行。

    这种方式的好处是,提高了并发度:没有线程在访问共享变量的时候需要等待,不同线程突破了“锁”机制的局限,可以同时安全地修改同一个数据结构的不同部分。尽管存在transaction提交失败所带来的开销,但是在实际的程序中,这种情况很少,在多核机器上,STM相对锁机制有大幅度的性能提升。

    但是,在实际中,STM系统也有性能不佳的情况,在核数较少的机器上(通常是1到4个,也取决于具体的应用),相对于基于细粒度锁机制的系统,STM系统的性能不如前者,但是最慢也不会超过两倍。性能损失的主要原因在于transaction需要维持log,同时提交也需要时间开销。STM的倡导者们相信相对于STM所带来的好处,这些开销是情有可原的。

    理论上来说,N个并发的transaction同时运行,则可能带来O(n)的时空开销。当然,实际的开销取决于实现的细节,但是在某些情况下(虽然很少),基于锁机制的系统比STM系统有更好的性能。

 

    2 概念上的优点和缺点

    除了带来性能上的提升,STM极大地简化了对于多线程程序概念上的理解,将传统的高层语言中的抽象概念,如对象和模块等等与多线程编程仍然可以有机的结合起来。另一方面,锁同步机制存在许多在实际编程中经常遇到的问题:

    程序员需要考虑许多操作之间是否重叠(数据竞争等等),即使这些操作分属于看上去毫不相干的不同的代码段。这个过程对于程序员来说很难,而且容易出错。

    程序员采用锁来防止死锁、活锁和其他bug,这种策略通常是强制的而且容易出错,而且产生的错误非常隐秘,难以复现和调试;

    锁机制可能会导致“优先权倒置”,亦即高优先级的线程必须等到低优先级的线程释放其所需要访问的资源。

 

    相对来说,STM更为简洁,因为每一个transaction可以被看作是单个线程在计算,死锁和活锁可以被完全的排除或者被外部的transaction manager所处理,程序员勿需操心。优先权倒置的问题也仍然会存在,但是高优先权的transaction可以终止到达提交阶段的低优先权的transaction。

 

 

    3 可组合的操作

    在2005年,Tim Harris, Simon Marlow, Simon Peyton Jones和Maurice Herlihy描述了一个建立在并发Haskell上的STM系统,该系统支持将任意的原子性操作组合成一个更大的原子性操作,而这些操作对于锁机制而言是不可能的。这里引用作者的原文如下:

“对于锁机制而言,可能最大的缺点就是不能组合:正确的代码片段如果组合起来则可能会失败。例如:考虑一个支持线程安全地插入和删除操作的哈希表,假设我们要删除表1中的一个项目A,并且将其插入到表2中,但是其中间状态(任何一个表都不包含A),不能被其他线程看到(否则可能会产生data race或者atomicty violation),除非程序员明确知道这点,否则无法满足这个操作的需求。简而言之,单个正确的操作(插入、删除等)无法组合成大的并且仍然是正确的操作。”

 

    使用STM的话,这个问题就可迎刃而解:将简单的将两个操作和在一个transaction当中,组合之后的操作仍然具有原子性。唯一棘手的问题是对于调用者(调用者不清楚组合之后的函数的实现细节)来说,当transaction提交失败的时候,从何处开始重新执行,为了解决这个问题,实现者们在STM中添加了“retry”这个命令,该命令使用log信息来判断transaction读取了哪些memory cells,如果当前memory cell被修改了,自动从被修改处重新执行。

    实现者同时也提出了另外一种可选的机制:orElse函数。它首先执行一个transaction,如果当前transaction执行了“retry”,则执行另外一个。如果两个都需要retry,则同时重新开始执行。这种方式,与POSIX网络调用select()相似,允许调用者等待众多同步事件的任何一个。这种方式同时简化了编程接口,例如它提供了一个简单的机制在阻塞和非阻塞操作之间进行转换。

    这个框架在Glasgow Haskell编译器已有实现。

 

 

    4 语言支持

    STM的语法正如其概念一样简捷,Tim Harris和Keir Fraser在其paper《Language Support for Lightweight Transactions》提出了使用传统的条件临界区(classical conditional critical region: CCR)来表示transaction。在这种最为简单的模式下,一段需要即时执行的代码组成了一个原子块(atomic block),例如:

 

    // 在双向链表中插入一个节点

    atomic {

     newNode->prev = node;

     newNode->next = node->next;

     node->next->prev = newNode;

     node->next = newNode;

     }

 

     当执行到block的结尾处时,transaction提交修改,或者终止、重试。CCR同时允许一个门控条件,该门控条件使得满足指定条件是才执行transaction:

 

    atomic (queueSize > 0) {

     remove item from queue and use it

     }

 

     如果门控条件不能满足,transaction manager在retry之前,将会等待直到另外一个transaction提交了修改,而且修改影响到门控条件的值。这种生产者和消费者之间的松散耦合的方式相对于线程之间显示通信的方式增强了系统的模块化。而“Composable Memory Transactions”使得为这种方式带来更进一步的好处(提供了上述的retry命令),使得在任何时候,可以终止一个transaction并且在retry之前等待直到之前读取的一些值被其他transaction修改。例如:

 

    atomic {

     if (queueSize > 0) {

         remove item from queue and use it

     } else {

         retry

     }

     }

 

     这种动态重试的方法简化了编程模型的同时为新的应用提供了可能性。

     但是仍然也有一些需要考虑的问题,其中之一就是当异常行为传播到了transaction之外的时候如何处理。

 

     未完待续 ...