百度空间 | 百度首页 
               
 
查看文章
 
Beautiful Concurrency Translation Part A
2007-03-10 00:36
This translation is posted with the permission of the author, Simon Peyton Jones. The English-language version appears in the book “Beautiful Code”, edited by Greg Wilson, published by O’Reilly, to appear in 2007.

本(中文)译本的发布行为,已经由原作者Simon Peyton Jones许可。原英文版本将载于《美丽的代码》,编辑为Greg Wilson,出版商为O'Reilly,将于2007年出版。

美丽的并发 Beautiful Concurrency

作者:Simon Peyton Jones,微软研究院,剑桥,2007年2月20日
译者:CloudiDust,东南大学,南京,2007年3月1日

原文地址:http://research.microsoft.com/~simonpj/papers/stm/beautiful.pdf

作者简介:Simon Peyton Jones,Haskell社区的重要领导者,GHC的主要贡献者,Hugs的主要实现者,STM Haskell的主要实现者,目前在英国剑桥微软研究院担任研究员。


===============================================================================


1. 引言

免费午餐结束了[11]。我们已经习惯于这样一个观念:通过购买下一代处理器,我们的程序就将会跑得更快。然而这个时代已经过去了。下一代芯片将集成更多的CPU,但同时单个CPU却不会跑得比上一年的型号更快。如果我们希望程序跑得更快,我们必须学习如何去编写并行程序[12]。

并行程序以不确定性的方式执行,因此这类程序难于调试,同时程序中的错误也几乎不可能再现。对于我而言,优美的程序,应是非常简洁而优雅,很明显没有错误的,而不是仅仅看不出明显错误来的[注1]。如果希望写出运行可靠的并行程序,我们必须对程序之美多加注意。很不幸的是,并行程序常常比他们串行的表亲丑陋些。尤其是,正如我们将要看到的,他们的模块性更弱。

在本章中(译者:因为这将是一本书的一部分,故如是说),我将描述 *软件事务内存*(Software Transactional Memory, STM) ,一种令人欢欣鼓舞的新方法。它被用于进行基于共享内存的并行多处理器的编程,它似乎能以一种当前技术所不能提供的方式来支持模块化程序。阅读完本章之后,我希望你能像我一样为STM而疯狂。它不是万灵药,但它是对并发这一令人畏惧的堡垒的一次漂亮而振奋人心的进攻。

---------------------------------------
[注1]这些短语是受到了Tony Hoare的启发。

( 译者:Tony Hoare原话:
There are two ways of constructing a software design. One is to make it so simple that there are obviously no deficiencies; the other is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult.
                                     -- The Emperor's Old Clothes, CACM February 1981

“软件设计有两种方式:一种是设计极为简洁,看得出明显没有缺陷;另一种是设计极为复杂,有缺陷也看不出来。第一种方式的难度要大得多。"
                                     —— 《皇帝的旧衣》,《ACM通讯》1981年2月号 )
---------------------------------------


===============================================================================



2. 简单示例:银行帐户

(译者:熟悉SICP的朋友会发现这个示例相当相当相当亲切哦!^_^)

这是一个简单的编程任务:

编写一个例程,将钱款从一个帐户转移到另一个帐户。简单起见,两个帐户都储存在内存中:不需要与数据库进行任何交互。这个例程必须能够在并发程序中正确地工作,在该程序中很多线程将可能同时调用transfer。任何一个线程都不允许观察到事务的中间状态:钱款已经从一个帐户转出,而还没有转入另一个帐户(反之亦然)。

这个例子有些不现实,但正因其简单性,我们能够集中关注以下新奇的东西: *Haskell语言* (第3.1小节)和 *事务内存* (第3.2小节)。但首先,让我们来简要地回顾一下通常的处理方法。

=======================================

2.1 使用锁的银行帐户

当前在并发程序调度中占有统治地位的技术是 *锁*(lock) 与 *条件变量*(conditional variable) 。在面向对象语言中,每个对象都具有一个隐式锁,而锁定操作通过 *同步方法*(synchronized method) 完成,但其思想别无二致。因此一个帐户类可以定义为:

class Account
{
int balance;
synchronized void withdraw( int n )
{
     balance = balance - n;
}
void deposit( int n )
{
     withdraw( -n );
}
}

我们必须小心地将withdraw声明为synchronized,从而当两个线程同时调用withdraw时,将不会出现某个取款操作丢失的错误。synchronized的作用是将帐户锁定,执行withdraw,然后释放锁。

现在,transfer可能写成这样:

void transfer( Account from, Account to, int amount )
{
from.withdraw( amount );
to.deposit( amount );
}

对串行程序而言,这段代码没有问题,但在并发程序中,另一个线程可能观察到一个中间状态:钱款已经从from中转出,但还没有转入to中。尽管两个方法(withdraw和deposit)都是synchronized,但这一点用处也没有。帐户from首先被withdraw方法锁定并解锁,而后被deposit方法锁定并解锁。在这两次调用之间,钱款(看起来)从这两个帐户中消失了。

在一个金融程序中,这将是不可接受的。该如何修复这个问题呢?通常的方案是添加显式锁定代码,如下:

void transfer( Account from, Account to, int amount )
{
from.lock(); to.lock();
from.withdraw( amount );
to.deposit( amount );
from.unlock(); to.unlock();
}

但这个程序将极易出现致命的死锁。特别地,考虑以下(并不常见的)状况:另一个线程正在同样的两个帐户之间进行相反方向的转帐操作。此时每个线程都将取得一个锁并等待对方解锁,从而陷入永远的阻塞之中。

一旦认识到这一点——问题并不总是很明显的——那么通常的修复方式便是在锁上附加一种任意的全局顺序,而后以升序请求它们。锁定代码将变为:

if from < to
then { from.lock(); to.lock(); }
else { to.lock(); from.lock(); }

当全部所需的锁都能被预见到时,这种方法工作得不错。但问题并不总是这样简单的。例如,假设from.withdraw被实现为:当from中的资金不足时,就从from_deposit(译者注:一个内部对象)中转出钱款。(在转帐时)我们并不知道是否应该请求from_deposit的锁,直到读取from中的数据之后才行。但此时(读取之后)若还想以“正确”的顺序来请求锁,为时已晚。更进一步说,from_deposit的存在是一个私有的问题,只能为from所知,而不能为transfer所知。即使transfer确实知晓from_deposit的存在,锁定代码也必须处理三个锁——大概是通过将它们排序的方式。

当我们希望使用阻塞操作时,事态变得更复杂了。例如,假设当from中资金不足时,transfer将发生阻塞。这一般是通过等待一个条件变量,同时释放from的锁来实现的。如果我们希望阻塞持续至from中有足够资金为止,同时将from_deposit纳入考虑范围,那么情况还会变得更加诡异。

=======================================

2.2 锁是糟糕的

长话短说,当前占统治地位的并发式编程技术——锁与条件变量——存在根本性的缺陷。以下是几点典型的困难,有一些已经在上文中提及:

*获取太少的锁*:程序员很容易忘记去获取锁,最终导致两个线程同时修改同一个变量。

*获取太多的锁*:程序员很容易获取太多锁,从而阻止了程序的并发(最好的情况),或者造成死锁(最糟的情况)。

*获取错误的锁*:在基于锁的编程中,锁与其保护的数据之间的联系常常只存在于程序员的头脑中,而不是显式存在于程序中。其结果是,获取或保持错误的锁实在是太容易了。

*以错误的顺序获取锁*:在基于锁的编程中,程序员必须小心翼翼地以“正确”的顺序获取锁。避免死锁的工作总是令人厌倦,错误丛生,并且有时是极端困难的。

*错误恢复*:会变得非常困难,这是由于程序员必须保证,没有任何错误将把系统置于一个不一致的,或是锁的保持状况不确定的状态下。

*丢失的唤醒与错误百出的重试*:程序员很容易忘记更新条件变量,而此时有线程正在等待该变量。程序员也很容易在唤醒线程之后对条件进行重复测试。

但是,基于锁的编程其最根本的缺点是: *锁与条件变量不支持模块化编程。* 说到“模块化编程”时,我指的是通过胶合小程序构造大程序的过程。锁摧毁了其可能性。例如,我们无法在不修改withdraw和deposit的(正确)实现的情况下,使用他们来实现transfer。取而代之地,我们必须暴露锁协议。阻塞与选择甚至更为反模块化。例如,假设我们有一个当源帐户资金不足时将阻塞的withdraw版本,那么在不暴露阻塞条件的情况下,我们将不能直接使用该withdraw来从A或者B取出钱款(选择A或B取决于哪个帐户资金充足)——甚至即使暴露了条件,要实现这个功能也还是不容易。在其他文献中,对这一批判有详细的阐述[7,8,4]。


===============================================================================


3. 软件事务内存(STM)

软件事务内存是应对并发挑战的一种激动人心的新方法,正如我将在这一小节中解释的一样。我将使用Haskell——我所知的最美的编程语言——来解释STM,这是由于STM与Haskell的配合尤其优雅。如果你不懂Haskell也不用担心,我们将一边讨论,一边学习。

=======================================

3.1 副作用与Haskell的输入输出:

这是用Haskell实现的transfer代码的开头部分:

transfer :: Account -> Account -> Int -> IO ()
-- Transfer ’amount’ from account ’from’ to account ’to’
transfer from to amount = ...

定义的第二行以“--”开头,这是注释。第一行给出了transfer的 *类型签名* 。这个签名指出,“transfer取两个类型为Account的值(源与目标帐户)以及一个Int(转帐金额)为参数[注2],并返回一个类型为IO()的值”。类型()读作“unit”(单位类型),该类型只有一个值,该值也写作()。这个类型类似于C语言中的void,因此transfer的返回类型IO()表明调用它的唯一原因便是为了其副作用。在进行进一步讨论之前,我们必须解释在Haskell中副作用是如何处理的。

一个“副作用”是任何读取或写入可变状态的事物。输入/输出是“副作用”的明显例子。例如,以下是两个有输入/输出副作用的Haskell函数的签名:

hPutStr :: Handle -> String -> IO ()
hGetLine :: Handle -> IO String

我们称任何类型为IO t的值为一个“操作”,因此(hPutStr h "hello")[注3]便是一个操作,当其执行时,将在句柄h[注4]上打印“hello”,并返回单位值。类似地,(hGetLine h)也是一个操作,当其执行时,将从句柄h读取一行输入并作为字符串返回。可以使用Haskell的do标记法将带副作用的小程序胶合为带副作用的大程序。例如,hEchoLine从输入(句柄)读取一个字符串并打印之:

hEchoLine :: Handle -> IO String
hEchoLine h = do { s <- hGetLine h
                 ; hPutStr h ("I just read: " ++ s)
                 ; return s }

标记do {a1;...;an}通过依序胶合小操作a1...an,构造出一个大操作。因此hEchoLine h是这样一个操作:当执行时,它将首先执行hGetLine h来从h读入一行,将结果命名为s,而后执行hPutStr以输出s,并以“ I just read: "开头[注5]。最终,它将字符串s返回。最后一行相当有趣,因为return并不是一个内建的语言结构,而是一个完完全全普普通通的函数,其类型是:

return :: a -> IO a

当操作return v执行时,将返回v,并且不产生任何副作用[注6]。该函数可以作用于任何类型的值之上,这一点通过其类型中的类型变量a标示出来。

输入/输出是副作用中相当重要的一分子,另一类重要的副作用是读写可变变量的操作。例如,这是一个递增可变变量取值的函数:

incRef :: IORef Int -> IO ()
incRef var = do { val <- readIORef var
                ; writeIORef var (val+1) }

IORef t类型的值应被视为指针或引用,它指向某个存储着可变的,类型为t的数据的单元。这有些类似于C中的类型(t*)。在incRef中,参数的类型为IORef Int,这是由于incRef只适用于存储着Int类型数值的单元。

到现在为止,我已经解释了如何胶合小操作以构造大操作——但操作是如何被实际执行的呢?在Haskell中,整个程序定义了单个IO操作,称为main。运行程序便是执行操作main,例如,这是一个完整的程序:

main :: IO ()
main = do { hPutStr stdout "Hello"
          ; hPutStr stdout " world\n" }

这是一个串行程序,因为do标记将操作依序组合了。若希望构造并发程序,需要使用一个新的原语,forkIO:

forkIO :: IO a -> IO ThreadId

函数forkIO是内建于Haskell语言之中的,它取一个IO操作为参数,并为其生成一个并发的Haskell线程。一旦线程生成,Haskell的运行时系统将使其与所有其余线程一道并发执行。例如,假设我们将main程序改为这样[注7]:

main :: IO ()
main = do { forkIO (hPutStr stdout "Hello")
          ; hPutStr stdout " world\n" }

现在这两个hPutStr操作将并发执行,且不能确定哪一个将会“胜利”(先打印其字符串)。通过forkIO生成的Haskell线程是极其轻量级的:它们只占用几百字节的内存。因此,单个程序生成数千个线程也是很有道理的。

亲爱的读者,现在你很可能会觉得Haskell是相当笨拙而冗长的语言。毕竟incRef的三行定义所做的并不比C中的x++更多。确实,在Haskell中副作用都是显式的而有些冗长的。但是,要牢记Haskell首先是一个 *函数式* 语言。大部分程序是使用Haskell的函数式核心写成的,它们富于表达力且简明扼要。从而,Haskell和善地(译者:真的?;)鼓励你去编写谨慎地使用副作用的程序。

其次,请注意使副作用显式化可以展现出很多有用的信息。考虑这两个函数:

f :: Int -> Int
g :: Int -> IO Int

只需要看过它们的类型,我们便可以知道f是一个纯函数:它没有副作用。给定一个Int,比如说42,那么每次调用(f 42)都将返回相同的值。相反地,g有副作用,而这在它的类型中表露得相当明显。每次执行g都可能返回不同的值——例如它可能从stdin读取数据,或修改一个可变变量——即使它的参数次次相同。这一将副作用显式化的能力在接下来的内容中将被证明是非常有用的。

最后,操作是 *第一等值*(first-class values) ,它们可以作为参数传递也可以作为结果返回。例如,这是一个(简化的)for循环函数的定义,完全使用Haskell写出,而不是内建的:

nTimes :: Int -> IO () -> IO ()
nTimes 0 do_this = return ()
nTimes n do_this = do { do_this; nTimes (n-1) do_this }

这一递归函数取一个表示循环次数的Int以及一个操作do_this并返回一个大操作:当该操作执行时,将执行do_this n次。例如,使用nTimes来将"Hello"打印10次:

main = nTimes 10 (hPutStrLn stdout "Hello")

将操作视为第一等值,其效果是使Haskell能够支持 *用户自定义的控制结构* 。(译者:受限的Lisp Macro?)

在本章中并不适合作Haskell的完整介绍,甚至也不适合作Haskell中副作用的完整介绍。若希望更进一步的阅读,教程“Tackling the awkward squad”[9]将是一个很好的起点。

---------------------------------------
[注2]你也许会认为,在这个类型签名中出现了三个箭头而不是一个是相当怪异的。这是因为Haskell支持curry操作。你可以在任何一本有关Haskell的书籍中(例如[13]),或wikipedia上找到其描述。在本论文中,只需简单地将除了最后一个之外的所有类型当作参数就可以了。

[注3]在Haskell中,书写函数应用只需简单并列即可。在大多数语言中可能需要写hPutStr(h, "hello"),但在Haskell中只需写(hPutStr h "Hello")。

[注4]Haskell中的“句柄”扮演C中“文件描述符”的角色,它指出进行读写操作的文件或管道。就像Unix中一样,有三个预定义句柄:stdin,stdout和stderr。

[注5](++)运算符用于连接两个字符串。

[注6]IO类型标明了产生副作用的 *可能性* ,而不是 *必然性* !

[注7]我们可以将main的第一行写作tid <- forkIO (hPutStr ...)以把forkIO所返回的ThreadId绑定到tid上。但既然我们在此不使用返回的ThreadId,那么就可以省略“tid <-"部分。
---------------------------------------

=======================================

3.2 Haskell中的事务

现在回到transfer函数上来,代码如下:

transfer :: Account -> Account -> Int -> IO ()
-- Transfer ’amount’ from account ’from’ to account ’to’
transfer from to amount
= atomically (do { deposit to amount
                   ; withdraw from amount })

其中的do块现在应能够很好地自我描述了:我们调用deposit将amount转入to,同时调用withdraw将amount转出from。待会儿我们将编写这些辅助函数,现在请注意对atomically的调用。它以一个操作为参数,并将其原子化执行。更准确地说,它提供了两大保证:

*原子性*(Atomicity):atomatically act的效果对另一个线程而言是同时可见的。这就保证了没有任何其他线程能够看到这样一个状态:钱款已经转入to,却还没有转出from。

*隔离性*(Isolation):在atomatically act的调用过程中,act操作完全不会受其他线程的影响。这就像是act在开始运行时取得一份当前状态的快照,而后在其上执行一样。

这是一个简单的atomatically执行模型:假设有一个单一的全局锁,atomatically act取得该锁,执行act操作,而后释放锁。这个实现粗暴地确保了没有任何两个原子块能够同时执行,因此确保了原子性。

这个模型有两个问题。其一,它完全不能确保隔离性:当一个线程正在处理某原子块中的IORef时(该线程保有全局锁),完全没有办法阻止另一个线程直接写入同一个IORef(即:在atomatically之外,并不保有全局锁)。因而,隔离性保证便被破坏了。其二,该模型的性能糟糕透了。这是由于,即使实际上并不会互相干涉,所有原子块也都将被序列化执行。

很快我将在第3.3小节讨论第二个问题。现在,第一项异议可以很简单地使用类型系统解决。我们给定automatically的类型为:

atomically :: STM a -> IO a

atomically的参数是一个STM a类型的操作。一个STM操作就像IO操作一样可以有副作用,但STM所允许的副作用范围要小得多。在STM操作中所做的最主要的事,便是读写类型为TVar a的 *事务变量*(transactional variable),这很类似于我们可以在IO操作中读写IORef[注8]。

readTVar :: TVar a -> STM a
writeTVar :: TVar a -> a -> STM ()

STM操作可以使用与IO操作相同的do标记来组合——do标记已经重载过,以便能在这两种类型上使用。return也是如此[注9]。例如,withdraw的代码如下:

type Account = TVar Int

withdraw :: Account -> Int -> STM ()
withdraw acc amount
= do { bal <- readTVar acc
       ; writeTVar acc (bal - amount) }

我们使用一个包含Int的事务变量来表示帐户,该Int代表帐户中的余额。withdraw是一个将帐户余额减少amount的STM操作。

为了补全transfer的定义,我们使用withdraw来定义deposit:

deposit :: Account -> Int -> STM ()
deposit acc amount = withdraw acc (- amount)

注意,transfer最终执行了四个原语读写操作:对帐户to的一次读/写,而后是对帐户from的一次读/写。这四个操作是原子化执行的,这符合第2节开头所给出的规范。

类型系统很优美地阻止了在事务外部对TVar的读写操作。例如,假设我们尝试以下代码:

bad :: Account -> IO ()
bad acc = do { hPutStr stdout Withdrawing..."
             ; withdraw acc 10 }

这个程序将被驳回,因为hPutStr是一个IO操作,但withdraw却是一个STM操作,而这两者不能被组合在同一个do块中。但如果我们用一个atomatically调用来包装withdraw,便会一切顺利:

good :: Account -> IO ()
good acc = do { hPutStr stdout "Withdrawing..."
              ; atomically (withdraw acc 10) }

---------------------------------------
[注8]这里的术语是不一致的,为了一致性,应该或者使用TVar和IOVar,或者使用TRef和IORef。但在当前这个阶段,改变命名方法会造成分裂。不论好坏,我们现在使用TVar和IORef。

[注9]do标记和return的重载并不是为了支持IO和STM而专门搞出来的小把戏。实际上,IO和STM都是一种称为 *单子*(monad) [15]的通用模式的实例,而重载是通过Haskell十分通用的 *类型的分类*(type-class) 机制实现的。
---------------------------------------

=======================================

3.3 实现事务内存

为了使用STM,程序员所需的一切便是我之前描述过的,对原子性和隔离性的保证。即使如此,我经常发现使用一个合理的实现模型来引导自己的直觉是很有用的。因而在本节中我将简单地描述一个这样的实现。不过请牢记,这只是 *一种* 可能的实现方式而已。STM抽象的一个美妙之处,便是它提供了一个小巧而清晰的接口,而这个接口可以通过很多方式实现,有些会很简单,也有些会很复杂。

有一种特别吸引人的实现已经在数据库领域中很好地确立了,它被称为 *优化执行*(optimistic execution) 。当(atomically act)被执行时,将分配一个线程本地的 *事务日志*,初始为空。而后act会执行,并且不获取任何锁。执行act时,每次对writeTVar的调用都将把TVar的地址以及其新值写入日志中——并不是写入TVar本身。对readTVar的每次调用都将首先搜索日志信息(以应对TVar被某个早先的writeTVar写入的情况),如果没有找到相关的记录,那么就直接从TVar读取值,并将TVar(的地址)以及所读取的值记录进日志中。与此同时,别的线程可能也在执行他们各自的原子块,正像疯子一样读写TVar呢。

当act操作完成后,这个实现首先 *验证* 日志的有效性,如果验证成功,便 *提交* 日志。验证步骤将检验记录在日志中的每个readTVar,以确保日志中的值与当前TVar中的实际值相匹配。如果是这样的话,验证就通过了,而提交步骤取出日志中所有写操作(写入的值)并将它们写入真正的TVar中。

这些步骤的执行确实是不可分割的:STM实现将禁止中断,使用锁,或者使用比较-交换指令——用任何所需的方法来确保验证与提交对别的线程来说是真正不可分割的。这些全都是由STM实现来负责处理的,而程序员不需要知道或者关心这一点是如何实现的。

那么如果验证失败了呢?此时事务会观察到不一致的内存景象,因此我们放弃该事务,重新初始化日志,然后从头开始再把act执行一遍。这个过程被称为 *重新执行*(re-execution)。既然act的写操作都还没有被写入内存,重新执行是绝对安全的。但是,要注意很关键的一点:除了对TVar的读写之外,act不能再有其它副作用了。例如,考虑代码:

atomically (do { x <- readTVar xv
               ; y <- readTVar yv
               ; if x>y then launchMissiles
                        else return () })

其中launchMissiles :: IO()会造成极其严重的国际间的副作用。(译者:汗……)既然原子块在执行中是不获取锁的,若其它线程并发地修改xv和yv,那么原子块将观察到不一致的内存景象。如果发生这样的情况,那么发射导弹就是个大错误了。但也只有在那时(发射之后)才能发现验证失败,而事务必须重新执行。幸运的是,类型系统阻止我们在STM操作内部执行IO操作,因此上面的程序段将被类型检查器驳回。这是区分IO操作和STM操作所带来的又一大好处。

=======================================

3.4 阻塞与选择

到目前为止,我们所引入的原子块还不能完全地协调并发程序。它们缺乏两大关键设施:*阻塞* 与 *选择* 。在本节中我将描述基本的STM接口是如何精巧地以一种模块化的方式包含它们的。

我们假设,当一个线程试图从帐户中透支时(即取款额大于当前的余额)应当被阻塞。在并发程序中,类似这样的状况是很常见的。例如,当线程从空缓冲区中读取数据,或者等待某个事件时,应当被阻塞。通过加入单个函数retry,我们就可以在STM中实现这一功能,其类型为:

retry :: STM a

以下是withdraw的一个修改版本,当余额可能变为负数时它将被阻塞:

limitedWithdraw :: Account -> Int -> STM ()
limitedWithdraw acc amount
= do { bal <- readTVar acc
; if amount > 0 && amount > bal
       then retry
       else writeTVar acc (bal - amount) }

retry的语义是很简单的:当执行retry操作时,当前事务被放弃,而后会在之后的某个时间重试。立即重试当前事务也是正确的,但可能会很低效:帐户的状态可能还没有改变,因此事务会又一次调用retry。一个高效的STM实现也许能够阻塞该线程直到另一个线程写入acc。那么STM实现该如何知道应该等待acc呢?因为事务在retry之前读取了acc,而这一事实被很方便地记录在事务日志中。

limitedWithdraw中的条件判断符合一种很常见的模式:检查一个布尔条件是否被满足了,若不是的话便retry。这一模式可以很容易地抽象为函数check:

check :: Bool -> STM ()
check True = return ()
check False = retry

现在我们可以使用check重写limitedWithdraw,使其变得更简洁一些:

limitedWithdraw :: Account -> Int -> STM ()
limitedWithdraw acc amount
= do { bal <- readTVar acc
       ; check (amount <= 0 || amount <= bal)
       ; writeTVar acc (bal - amount) }

现在来考虑 *选择* 。假设你希望从帐户A中取款,但如果余额不足的话就去B中取(该如何实现呢)?为此,我们需要当第一个操作重试时选择一个替代操作的能力。为了支持选择操作,STM Haskell具有又一个原语操作,称为orElse,其类型为:

orElse :: STM a -> STM a -> STM a

正如automatically本身一样,orElse取操作作为参数,而后将它们胶合为一个大操作。它的语义如下:操作(orElse a1 a2)首先执行a1,如果a1重试了(即它调用了retry),那么就尝试执行a2,如果a2也重试了,那么整个操作进行重试。举例说明orElse的用法可能更简单一些:

limitedWithdraw2 :: Account -> Account -> Int -> STM ()
-- (limitedWithdraw2 acc1 acc2 amt) withdraws amt from acc1,
-- if acc1 has enough money, otherwise from acc2.
-- If neither has enough, it retries.
limitedWithdraw2 acc1 acc2 amt
= orElse (limitedWithdraw acc1 amt) (limitedWithdraw acc2 amt)

既然orElse的结果本身也是一个STM操作,你可以将其提供给另一个orElse,从而可以在任意数目的可选对象中进行选择。

=======================================

3.5 到目前为止的小结

在本小节中我介绍了STM Haskell所支持的所有关键的事务内存操作。它们被归纳在表1中。该表包含了到目前为止还没有用到的一个操作:newTVar,它用于创建新的TVar单元(cell)。我们将在下一小节中使用它。

---------------------------------------
atomically :: STM a -> IO a
retry :: STM a
orElse :: STM a -> STM a -> STM a
newTVar :: a -> STM (TVar a)
readTVar :: TVar a -> STM a
writeTVar :: TVar a -> a -> STM ()
---------------------------------------

表1:STM Haskell中的关键操作


点此阅读第二部分(Click here for Part B)

类别:函数阴阳 | 添加到搜藏 | 浏览() | 评论 (0)
 
最近读者:
 
网友评论:
发表评论:
姓 名:
网址或邮箱: (选填)
内 容:
验证码: 请点击后输入四位验证码,字母不区分大小写
      

     

©2009 Baidu