科技: 人物 企业 技术 IT业 TMT
科普: 自然 科学 科幻 宇宙 科学家
通信: 历史 技术 手机 词典 3G馆
索引: 分类 推荐 专题 热点 排行榜
互联网: 广告 营销 政务 游戏 google
新媒体: 社交 博客 学者 人物 传播学
新思想: 网站 新书 新知 新词 思想家
图书馆: 文化 商业 管理 经济 期刊
网络文化: 社会 红人 黑客 治理 亚文化
创业百科: VC 词典 指南 案例 创业史
前沿科技: 清洁 绿色 纳米 生物 环保
知识产权: 盗版 共享 学人 法规 著作
用户名: 密码: 注册 忘记密码?
    创建新词条
科技百科
  • 人气指数: 4923 次
  • 编辑次数: 2 次 历史版本
  • 更新时间: 2009-06-09
方兴东
方兴东
发短消息
方兴东
方兴东
发短消息
相关词条
戴夫·海厄特
戴夫·海厄特
最佳编程语录大全
最佳编程语录大全
程序员笑话大全
程序员笑话大全
下一代程序员
下一代程序员
女程序员
女程序员
彼得·诺维格
彼得·诺维格
Russ Cox
Russ Cox
15名程序员界性感的奇葩
15名程序员界性感的奇葩
Mike Kruzeniski
Mike Kruzeniski
Jeff Fong
Jeff Fong
推荐词条
希拉里二度竞选
希拉里二度竞选
《互联网百科系列》
《互联网百科系列》
《黑客百科》
《黑客百科》
《网络舆情百科》
《网络舆情百科》
《网络治理百科》
《网络治理百科》
《硅谷百科》
《硅谷百科》
2017年特斯拉
2017年特斯拉
MIT黑客全纪录
MIT黑客全纪录
桑达尔·皮查伊
桑达尔·皮查伊
阿里双十一成交额
阿里双十一成交额
最新词条

热门标签

微博侠 数字营销2011年度总结 政务微博元年 2011微博十大事件 美国十大创业孵化器 盘点美国导师型创业孵化器 盘点导师型创业孵化器 TechStars 智能电视大战前夜 竞争型国企 公益型国企 2011央视经济年度人物 Rhianna Pratchett 莱恩娜·普莱契 Zynga与Facebook关系 Zynga盈利危机 2010年手机社交游戏行业分析报告 游戏奖励 主流手机游戏公司运营表现 主流手机游戏公司运营对比数据 创建游戏原型 正反馈现象 易用性设计增强游戏体验 易用性设计 《The Sims Social》社交亮 心理生理学与游戏 Kixeye Storm8 Storm8公司 女性玩家营销策略 休闲游戏的创新性 游戏运营的数据分析 社交游戏分析学常见术语 游戏运营数据解析 iPad风行美国校园 iPad终结传统教科书 游戏平衡性 成长类型及情感元素 鸿蒙国际 云骗钱 2011年政务微博报告 《2011年政务微博报告》 方正产业图谱 方正改制考 通信企业属公益型国企 善用玩家作弊行为 手机游戏传播 每用户平均收入 ARPU值 ARPU 游戏授权三面观 游戏设计所运用的化学原理 iOS应用人性化界面设计原则 硬核游戏 硬核社交游戏 生物测量法研究玩家 全球移动用户 用户研究三部曲 Tagged转型故事 Tagged Instagram火爆的3大原因 全球第四大社交网络Badoo Badoo 2011年最迅猛的20大创业公司 病毒式传播功能支持的游戏设计 病毒式传播功能 美国社交游戏虚拟商品收益 Flipboard改变阅读 盘点10大最难iPhone游戏 移动应用设计7大主流趋势 成功的设计文件十个要点 游戏设计文件 应用内置付费功能 内置付费功能 IAP功能 IAP IAP模式 游戏易用性测试 生理心理游戏评估 游戏化游戏 全美社交游戏规模 美国社交游戏市场 全球平板电脑出货量 Facebook虚拟商品收益 Facebook全球广告营收 Facebook广告营收 失败游戏设计的数宗罪名 休闲游戏设计要点 玩游戏可提高认知能力 玩游戏与认知能力 全球游戏广告 独立开发者提高工作效率的100个要点 Facebook亚洲用户 免费游戏的10种创收模式 人类大脑可下载 2012年最值得期待的20位硅谷企业家 做空中概股的幕后黑手 做空中概股幕后黑手 苹果2013营收 Playfish社交游戏架构

西蒙·佩顿·琼斯 发表评论(0) 编辑词条

西蒙·佩顿·琼斯(Simon Peyton Jones)(born in South Africa on January 18, 1958) ,Haskell语言核心人物之一,并领导设计了著名的Haskell编译器GHC。

西蒙·佩顿·琼斯,硕士,于1980年毕业于剑桥大学三一学院。在工作两年后,他在伦敦大学学院担任了7年的讲师,然后在格拉斯哥大学担任了9年的教授,后来于1998年加入微软研究中心。他的研究领域包括函数式编程语言及其实现和应用。他领导了一系列的研究项目,主要研究用于单处理器机器和并行机的高质量函数式语言系统的设计和实现。他是函数式语言Haskell的主要设计者,此外他还是被广泛应用的Glasgow Haskell编译器(GHC)首席设计师。他还编写了两本关于函数式语言实现的教科书。

目录

[显示全部]

个人简介编辑本段回目录

Simon Peyton Jones (born in South Africa on January 18, 1958 [1]) is a British computer scientist who researches the implementation and applications of functional programming languages, particularly lazy functional languages. He is an honorary Professor of Computer Science at the University of Glasgow and supervises PhD Students at the University of Cambridge.

(图)Simon Peyton JonesSimon Peyton Jones

Peyton Jones graduated from Trinity College, Cambridge in 1980, and worked in industry for two years before serving as a lecturer at University College London and as a professor at the University of Glasgow, where he subsequently served as Head of the Department of Computer Science. He currently works at Microsoft Research in Cambridge, England. He is married to Dorothy, a priest in the Church of England, and they have three children.

He is a major contributor to the design of the Haskell language, and a principal designer of the Glasgow Haskell Compiler. He is also involved in the C-- project (which is used in GHC[1]).

He was also a major contributor to the 1999 book Cybernauts Awake which explored the ethical and spiritual implications of the Internet.

In 2004 he was inducted as a Fellow of the Association for Computing Machinery.

出版书籍编辑本段回目录

The Implementation of Functional Programming Languages. Prentice-Hall, 1987. ISBN 0-13-453333-X
Implementing Functional Languages, with David Lester. Prentice-Hall, 1992. ISBN 0-13-721952-0
Cybernauts Awake!, with Derek Burke, Nicholas Beale, David Pullinger, Harold Thimbleby, Christine Crosbie, Theresa Leal and others. Church House Publishing, 1999. ISBN 0-7151-6586-0
"Beautiful Concurrency", in Beautiful Code, edited by Andy Oram, Greg Wilson, O'Reilly, 2007. ISBN 0-596-51004-7

Simon Peyton Jones的50岁生日 编辑本段回目录

Mountain 写道 "Simon Peyton Jones今天50岁生日!Simon Peyton Jones,英国的计算机科学家,微软研究院成员,Glasgow大学荣誉教授,1958年的今天生于南非。他的研究领域是函数式程序设计语言的实现与应用。他是Haskell语言的设计者之一,GHC编译器的主要作者之一。他的课题也包括Software Transactional Memory。生日快乐Simon!"

第24章《美丽的并发》,作者:Simon Peyton Jones编辑本段回目录

免费午餐已经结束[1]。以往我们习惯于通过购买新一代CPU来加快程序的运行速度,但现在,这样的好时光已经一去不复返了。虽说下一代芯片会带有多个CPU,但每个单独的CPU的速度却不会再变快了。所以,如果想让程序跑得更快的话,你就必须得学会编写并行程序[2]。

(图)Simon Peyton JonesSimon Peyton Jones

[1]Herb Sutter, "The free lunch is over: a fundamental turn toward concurrency in software," Dr. Dobb's Journal, March 2005.
[2]Herb Sutter and James Larus, "Software and the concurrency revolution," ACM Queue, Vol. 3, No. 7, September 2005.
并行程序的执行是非确定性的,因而测试起来自然也就不容易;并发程序中有的bug甚至可能会无法重现。我对漂亮程序的定义是“简单而优雅,乃至于显然没有任何错误”,而不仅仅是“几乎没有任何明显的错误”[3]。要想编写出能够可靠运行的并行程序,程序的美感是尤其要注意的方面。可惜一般来说并行程序总归没有它们相应的非并行(顺序式的)版本漂亮;尤其是在模块性方面:并行程序的模块性相对较弱。这一点我们后面会看到。
[3]This turn of phrase is due to Tony Hoare.
本章将介绍软件事务内存(STM)。软件事务内存是一项很有前景的技术,它提出了一种针对共享内存并行处理器编程的新手段,正如刚才所言,传统并行程序的模块性较弱,而这正是软件事务内存的强项。我希望你读完本章后能和我一样对这项新技术感到振奋。当然,软件事务内存也并非万灵药,但它的确对并发领域中令人望而却步的问题发起了一次漂亮且令人鼓舞的冲击。
24.1  一个简单的例子:银行账户
假设有这样一个简单的编程任务:
编写一段程序,将钱从一个银行账户转移到另一个账户。为了简化起见,两个账户都是存放在内存里面的——也就是说你不用考虑跟数据库的交互。要求是你的代码在并发环境中也必须能够正确执行,这里所谓的并发环境也就是指可能存在多个线程同时调用你的转账函数,而所谓能够正确执行则是指任何线程都不能“看到”系统处于不一致的状态(比如看到其中一个账户显示已被取出了一笔钱然而同时另一个账户却显示还没有收到这笔钱)。
这个例子虽说有点人为捏造的味道,但它的优点是简单,因而我们便能将注意力集中在它的解决方案上,后面你会看到,Haskell结合事务内存将会给这个问题带来新的解决方案。不过此前还是让我们先来简单回顾一下传统的方案。
24.1.1 加锁的银行账户
目前,用于协调并发程序的主导技术仍是锁和条件变量。在一门面向对象的语言中,每个对象上都带有一个隐式的锁,而加锁则由synchronized方法来完成,但原理与经典的加锁解锁是一样的。在这样一门语言中,我们可以将银行账户类定义成下面这样:
       class Account {
        Int balance;
        synchronized void withdraw( Int n ) {
          balance = balance - n; }
        void deposit( Int n ) {
          withdraw( -n ); }
       }
withdraw方法必须是synchronized的,这样两个线程同时调用它的时候才不会把balance减错了。synchronized关键字的效果就等同于先对当前账户对象加锁,然后运行withdraw方法,最后再对当前账户对象解锁。
有了这个类之后,我们再来编写transfer转账函数:
       void transfer( Account from, Account to, Int amount ) {
        from.withdraw( amount );
        to.deposit( amount ); }
对于非并发程序来说以上代码完全没问题,然而在并发环境中就不一样了,另一个线程可能会看到转账操作的“中间状态”:即钱从一个账户内被取走了,而同时另一个账户却还没收到这笔钱。值得注意的是,虽然withdraw和deposit这两个函数都是synchronized,但这种情况还是会出现。在from上调用withdraw会将from加锁,执行提款操作,然后对其解锁。类似的,在to上调用deposit会将to加锁,执行存款操作,然后对其解锁。然而,关键是在这两个调用之间有一个间隙,在这个状态下待转账的钱既不在from账户中也不在to账户中。
在一个金融程序中,这种情况可能是无法容忍的。那我们该如何解决这个问题呢?通常的方案可能是在外面再包一层显式的加锁解锁操作,如下:
       void transfer( Account from, Account to, Int amount ) {
        from.lock(); to.lock();
          from.withdraw( amount );
          to.deposit( amount );
        from.unlock(); to.unlock() }
但这种做法有一个致命的缺陷:它可能会导致死锁。我们考虑这样一种情况:在一个线程将一笔钱从A账户转到B账户的同时,另一个线程也正在将一笔钱从B账户转到A账户(当然,发生这种事情的几率很小)。这时便可能会出现两个线程各锁一个账户并都在等着对方释放另一账户的情况。
问题找出来了(不过,并发环境下的问题可不总是像这么容易就能找出来的),标准的解决方案是规定一个全局统一的锁顺序,并按照递增顺序来进行加锁。采用这种做法的代码如下:

(图)Simon Peyton Jones and Philip WadlerSimon Peyton Jones and Philip Wadler

       if from < to
        then { from.lock(); to.lock(); }
        else { to.lock(); from.lock(); }
这个方法是可行的,但前提是必须事先知道要对哪些锁进行加锁,而后面这个条件并不是总能满足的。例如,假设from.withdraw的实现当账户余额不足时就会从from2上提款。遇到这种情况除非等到我们从from中提了钱否则是无法知道是否该对from2加锁的,而另一方面,一旦已经从from中提了钱,再想按“正确”顺序加锁便不可能了。更何况from2这个账户可能根本就只应对from可见,而不应被transfer知道。而且退一步说,就算transfer知道from2的存在,现在需要加的锁也已经由两个变成了三个(事先还要记得将这些锁正确排序)。
还有更糟的,如果我们需要在某些情况下阻塞(block),情况就会更加复杂。例如,要求transfer在from账户内余额不足的时候阻塞。这类问题通常的解决办法是在一个条件变量上等待,并同时释放from的锁。但更进一步,如果要求当from和from2中的总余额不够的时候阻塞呢?
24.1.2 “生锈”的锁
简而言之,在如今的并发编程领域占主导地位的技术,锁和条件变量,从根本上是有缺陷的。以下便是基于锁的并发编程中的一些公认的困难(其中有些我们在前文的例子中已经看到过了)。
锁加少了
容易忘记加锁,从而导致两个线程同时修改同一个变量。

(图)Simon Peyton-JonesSimon Peyton-Jones


锁加多了
容易加锁加得过多了,结果(轻则)妨碍并发,(重则)导致死锁。
锁加错了
在基于锁的并发编程中,锁和锁所保护的数据之间的联系往往只存在于程序员的大脑里,而不是显式地表达在程序代码中。结果就是一不小心便会加错了锁。
加锁的顺序错了
在基于锁的并发编程中,我们必须小心翼翼地按照正确的顺序来加锁(解锁),以避免随时可能会出现的死锁;这种做法既累人又容易出错,而且,有时候极其困难。
错误恢复
错误恢复也是个很麻烦的问题,因为程序员必须确保任何错误都不能将系统置于一个不一致的、或锁的持有情况不确定的状态下。
忘记唤醒和错误的重试
容易忘记叫醒在条件变量上等待的线程;叫醒之后又容易忘记重设条件变量。
然而,基于锁的编程,其最根本的缺陷,还是在于锁和条件变量不支持模块化编程。这里“模块化编程”是指通过粘合多个小程序来构造大程序的过程。而基于锁的并发程序是做不到这一点的。还是拿前面的例子来说吧:虽然withdraw和deposit这两个方法都是并发正确的,但如果原封不动的话,你能直接用它们实现出一个transfer来吗?不能,除非让锁协议暴露出来。而且遇到选择和阻塞的话还会更头疼。例如,假设withdraw在账户余额不足的情况下会阻塞。你就会发现,除非暴露锁条件,否则你根本无法直接利用withdraw函数从“A账户或B账户(取决于哪个账户有足够余额)”进行提款;而且就算知道了锁条件,事情仍还是麻烦。另一些文献中也对锁并发的这种困难作了论述。[4]
[4]Edward A. Lee, "The problem with threads,"IEEE Computer, Vol. 39, No. 5, pp. 33–42, May 2006; J. K. Ousterhout, "Why threads are a bad idea (for most purposes)," Invited Talk, USENIX Technical Conference, January 1996; Tim Harris, Simon Marlow, Simon PeytonJones, and Maurice Herlihy, "Composable memory transactions,"ACM Symposium on Principles and Practice of Parallel Programming (PPoPP '05), June 2005.
24.2 软件事务内存
软件事务内存(STM)是迎接并发挑战的一种很有前途的新技术,本节将会详细说明这一点。我选用Haskell来介绍STM,Haskell是我见过的最美丽的编程语言,而且STM能够特别优雅地融入到Haskell中。如果你还不了解Haskell,别担心,边看边学。

24.2.1  Haskell中的副作用(Side Effects)和输入/输出(I/O)
Haskell中的transfer函数写出来就像这样:

       transfer :: Account -> Account -> Int -> IO ( )       -- Transfer 'amount' from account 'from' to account 'to'       transfer from to amount = ...
以上代码的第二行,即以“--”开头的那行,是一个注释。代码的第一行是对transfer的函数类型的声明(类型声明以“::”前导)[5],“Account -> Account -> Int -> IO ( )”读作“接受一个Account,加上另一个Account(两个Account,代表转账的源账户和目标账户),以及一个Int(转账的数额),返回一个IO( )类型的值”。最后一个类型(即返回类型)“IO( )”说的是“transfer函数返回的是一个动作(action),这个动作被执行的时候可能会产生副作用(side effects),并返回一个‘( )’类型的值”。“( )”类型读作“单元(unit)”,该类型只有一个可能的值,也写作“( )”,有点类似于C里面的void。transfer将IO( )作为返回类型说明了执行过程中的副作用是我们调用transfer的惟一原因。那么,在介绍下面的内容之前,我们就必须首先知道Haskell是怎么对待副作用的。

[5]你可能会觉得奇怪,为什么在这个类型签名里面有三个“->”(难道不应该是一个吗——位于参数类型与返回类型之间?)其实这是因为Haskell支持所谓的currying,后者在任何介绍Haskell的书(比如Haskell: The Craft of Functional Programming, by S.J. Thompson [Addison-Wesley])中或wikipedia上都能见到它的踪影。但currying不是本章要讲的重点,你大可以忽略除了最后一个“->”之外的所有“->”,即除了最后一个类型之外,其他都是函数的参数类型。
那么副作用是什么呢?副作用就是我们读写可变(mutable)状态所造成的影响(effect)。输入/输出是副作用的绝佳范例。例如,下面是两个Haskell函数,它们都具有输入/输出副作用:

       hPutStr :: Handle -> String -> IO ()       hGetLine :: Handle -> IO String任何类型形如“IO t”(其中t可以为( ),也可以为其他类型,如String)的值都是一个动作(action)。也就是说,在上面的例子中,(hPutStr h “hello”)是一个动作[1],执行这个动作的效果便是在句柄h上输出“hello”[2]。类似的,(hGetLine h)也是一个动作,当它被执行的时候就会从句柄h代表的输入设备中读入一行输入并将其作为一个String返回。此外,利用Haskell的do关键字,我们可以将几个小的带副作用的程序“粘合”成一个大的。例如,下面的hEchoLine函数读入一个串并将它打印出来:

[1]在Haskell里面调用一个函数很简单,只须将函数名和它的各个参数并排写在一块儿就行了。在大多数语言中你都需要写成hPutStr(h, “hello”),但Haskell里面只要写成(hPutStr h “hello”)就行了。
[2]Haskell中的句柄相当于C里面的文件描述(file descriptor):指明对哪个文件或管道进行读写。跟Unix里面一样,Haskell里面也预定义了三个句柄:stdin、stdout和stderr。
       hEchoLine :: Handle -> IO String       hEchoLine h = do { s <- hGetLine h                       ; hPutStr h ("I just read: " ++ s ++ "\n")                       ; return s }do{a1; …;an}结构将数个较小的动作(a1…an)粘合成一个较大的动作。因此在上面的代码中,hEchoLine h这个动作被执行的时候便会首先调用hGetLine h来读取一行输入,并将这行输入命名为s。接着调用hPutStr,将s加上前导的“I just read:”[3]一并打印出来。最后串s被返回。最后一行return s比较有趣,因为return并非像在其他命令式语言中那样是语言内建的操作,而只是一个普普通通的函数,其函数类型如下:

[3]++操作符的作用是将两个串拼接起来。

(图)Simon Peyton JonesSimon Peyton Jones

       return :: a -> IO a
也就是说,像return v这样一个操作被执行的时候,将会返回v,同时并不会导致任何副作用[9]。return函数可以作用于任何类型的值,这一点体现在其函数类型中:a是一个类型变量,代表任何类型。

[9]“IO”的意思是一个函数可能有副作用,但并不代表它就一定会带来副作用。
输入/输出是一类重要的副作用。还有一类重要的副作用便是对可变(mutable)变量的读写。例如,下面这个函数将一个可变变量的值增1:

       incRef :: IORef Int -> IO ( )       incRef var = do { val <- readIORef var                      ; writeIORef var (val+1) }incRef var是一个动作。它首先执行readIORef var来获得变量var的值,并将该值绑定到val;接着它调用writeIORef将val+1写回到var里面。readIORef和writeIORef的类型如下:

       readIORef :: IORef a -> IO a       writeIORef :: IORef a -> a -> IO ( )类型形如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" }该程序是一个顺序式(sequential)程序,因为do块将两个IO动作按顺序连接了起来。另一方面,要构造并发程序的话,我们便需要另一个原语(primitive):forkIO:

       forkIO :: IO a -> IO ThreadIdforkIO是Haskell内建的函数,它的参数是一个IO动作,forkIO所做的事情就是创建一个并发的Haskell线程来执行这个IO动作。一旦这个新线程建立,Haskell的运行时系统便会将它与其他Haskell线程并行执行。例如假设我们将前面的main函数修改为[1]:

[10]其实,main的第一行我们本可以写成“tid <- forkIO(hPutStr …)”的,这行语句会把forkIO的返回值(一个ThreadId)绑定到tid。然而由于本例中我们并不使用返回的ThreadId,所以就把“tid <-”省略了。
       main :: IO ( )       main = do { forkIO (hPutStr stdout "Hello")                ; hPutStr stdout " world\n" }现在,这两个hPutStr操作便能够并发执行了。至于哪个先执行(从而先打印出它的字符串)则是不一定的。Haskell里面由forkIO产生出来的线程是非常轻量级的:只占用几百个字节的内存,所以一个程序里面就算产生上千个线程也是完全正常的。

读到这里,你可能会觉得Haskell实在是门又笨拙又麻烦的语言,incRef的三行代码说穿了就做了C里面的一个x++而已!没错,在Haskell里面,实施副作用的方式是非常显式且冗长的。然而别忘了,首先Haskell主要是一门函数式编程语言。大多数代码都是在Haskell的函数式内核里写的,后者的特点是丰富、高表达力、简洁。因而Haskell编程的精神就是“有节制地使用副作用”。

其次我们注意到,在代码中显示声明副作用的好处是代码能够携带许多有用的信息。考虑下面两个函数:

       f :: Int -> Int       g :: Int -> IO Int通过它们的类型我们便可以一眼看出,f是一个纯函数,无副作用。给它一个特定的数(比如42),那么每次对它的调用(f 42)都会返回同样的结果。相比较之下,g就具有副作用了——这一点明明白白地显示在它的类型中。对g的不同的调用,就算参数相同,也可能会得到不同的值,因为,比如说,它可以通过stdin读取数据,或对一个可变变量进行修改。后面你会发现,这种显式声明副作用的做法其实非常有用。

最后,前面提到的所谓动作(action,比如I/O动作)其实本身也是值(Haskell中的动作也是一等公民):它们也可以被作为参数传递或作为返回值返回。比如下面就是一个纯粹用Haskell写的(而非内建的)模拟(简化的)for循环的函数:

       nTimes :: Int -> IO ( ) -> IO ( )       nTimes 0 do_this = return ( )       nTimes n do_this = do { do_this; nTimes (n-1) do_this }这是一个递归函数,其第一个参数是一个Int,表示要循环多少次,第二个参数则是一个动作(action):do_this;该函数返回一个动作,后者被执行的时候会把do_this重复做n遍。比如下面这段代码利用nTimes来重复输出10个“Hello”:

       main = nTimes 10 (hPutStr stdout "Hello\n")这其实从效果上就等同于允许用户自定义程序的控制结构了。

话说回来,本章的目的并不是要对Haskell作一个全面的介绍,而且即便是对于Haskell里面的副作用我们也只是稍加阐述。如果你想进一步了解的话,可以参考我写的一篇指南“Tackling the awkward squad”[11]。

[11]Simon PeytonJones, "Tackling the awkward squad: monadic input/output,concurrency, exceptions, and foreign-language calls in Haskell," C. A. R. Hoare, M. Broy, and R. Steinbrueggen, editors,Engineering theories of software construction, Marktoberdorf Summer School 2000, NATO ASI Series, pp. 47–96, IOS Press, 2001.
24.2.2 Haskell中的事务
OK,终于可以回到我们的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账户,然后再从from账户中提取amount数目的钱。至于deposit和withdraw这两个辅助函数我们待会再来写,现在我们先来看看对atomically的调用。atomically的参数是一个动作,它会将该动作作为一个原子来执行。更精确地说,atomically有如下两个特性作保证:

原子性

atomically act调用所产生的副作用对于其他线程来说是“原子的”。这就保证了另一个线程不可能观察到钱被从一个账户中取出而同时又没来得及存入另一个账户中去的中间状态。

隔离性

在atomically act执行的过程中,act这个动作与其他线程完全隔绝,不受影响。这就好像在act开始执行的时候世界停顿了,直到act执行完毕之后世界才又开始恢复运行。

至于atomically函数的执行模型,简单的做法是:存在一个惟一的全局锁;atomically act首先获取该锁,然后执行动作act,最后释放该锁。这个实现虽然保证了原子性,但粗暴地禁止了任意两个原子块在同一时间执行。

上面说的这个模型有两个问题。第一,它并没有保证隔离性:比如一个线程在执行一个原子块的过程中访问了一个IORef(此时该线程持有全局锁),另一个线程此时照样可以直接对同一个IORef进行写操作(只要这个写操作不在原子块内)。这就破坏了隔离性保证。第二,它极大的损害了执行性能,因为即便各个原子块之间互不相干,也必须被串行化执行。

第二个问题待会在“事务内存实现”一节会详细讨论。目前先来看第一个问题。第一个问题可以通过类型系统轻易解决。我们将atomically函数赋予如下类型:

       atomically ::STM a -> IO aatomically的参数是一个类型为STM a的动作。STM动作类似于IO动作,它们都可能具有副作用,但STM动作的副作用的容许范围要小得多。STM中你可以做的事情主要就是对事务变量(类型为TVar a)进行读写,就像我们在IO动作里面主要对IORef进行读写一样[12]。

[12]这儿其实有一个命名上的不一致:STM变量被命名为TVar,然而普通变量却被命名为IORef——其实要么应该是TVar/IOVar,要么应该是TRef/IORef才对。但事到如今已经没法再改了。
       readTVar :: TVar a -> STM a       writeTVar :: TVar a -> a ->STM ( )跟IO动作一样,STM动作也可以由do块组合起来,实际上,do块针对STM动作进行了重载,return也是;这样它们便可以运用于STM和IO两种动作了[13]。例如,下面是withdraw的代码:

[13]其实Haskell并没有特别针对IO和STM动作来重载do和return,IO和STM其实只是一个更一般的模式的特例,这个更一般的模式便是所谓的monad(P. L. Wadler在“The essence of functional programming”20th ACM Symposium on Principles of Programming Languages [POPL '92], Albuquerque, pp. 1–14, ACM, January 1992中有描述),do和return的重载便是通过用Haskell的非常泛化的“类型的类型”(type-class)系统来表达monad而得以实现的(described in P. L. Wadler and S. Blott, "How to make ad-hoc polymorphism less ad hoc,"Proc 16th ACM Symposium on Principles of Programming Languages, Austin, Texas, ACM, January 1989; and Simon Peyton Jones, Mark Jones, and Erik Meijer, "Type classes: an exploration of the design space," J. Launch-bury, editor,Haskell workshop, Amsterdam, 1997)。
       type Account = TVar Int        withdraw :: Account -> Int -> STM ( )       withdraw acc amount        = do { bal <- readTVar acc             ; writeTVar acc (bal - amount) }我们用一个包含一个Int(账户余额)的事务变量来表示一个账户。withdraw是一个STM动作,将账户中的余额提走amount。

为了完成transfer的定义,我们可以通过withdraw来定义deposit:

       deposit :: Account -> Int -> STM ( )       deposit acc amount = withdraw acc (- amount)注意,transfer从根本上执行了四个基本的读写操作:对to账户的一次读和一次写;以及对from账户的一次读和一次写。这四个操作是被当成一个原子来执行的,其执行满足本节(“一个简单的例子:银行账户”)开头的要求。

Haskell的类型系统优雅地阻止了我们在事务之外读写TVar。例如假设我们这样写:

       bad :: Account -> IO ( )       bad acc = do { hPutStr stdout "Withdrawing..."                   ; withdraw acc 10 }以上代码不能通过编译,因为hPutStr是一个IO动作,而withdraw则是一个STM动作,这两者不能放在同一个do块中。但如果我们把withdraw再放在一个atomically调用当中就可以了,如下:

(图)Simon Peyton JonesSimon Peyton Jones

       good :: Account -> IO ( )       good acc = do { hPutStr stdout "Withdrawing..."                    ; atomically (withdraw acc 10) }24.2.3 事务内存实现
有了前面提到过的原子性和隔离性保证,我们其实便可以放心使用STM了。不过我常常还是觉得一个合理的实现模型会给直觉理解带来很大的帮助,本节就来介绍这么一个实现模型。但要注意的是,这只是所有可能实现中的一种。STM抽象的一个漂亮之处就在于它提供了一个小巧干净的接口,而实现这个接口可以有多种方式,可简单可复杂。

在可行的实现方案中,有一个方案特别吸引人,那就是在数据库实现里被采用的所谓的“乐观执行(optimistic execution)”。当atomically act被执行的时候,Haskell运行时系统会为它分配一个线程本地的事务日志,该日志最初的时候是空的,随着act动作被一步步执行(其间并不加任何形式的锁),每次对writeTVar的调用都会将目标TVar变量的地址和新值写入日志;而并不是直接写入到那个TVar变量本身。每次对readTVar的调用都会首先寻找日志里面有没有早先被写入的新值,没有的话才会从目标TVar本身读取,并且,在读取的时候,一份拷贝会被顺便存入到日志中。同一时间,另一个线程可能也在运行着它自己的原子块,疯狂地读写同样一组TVar变量。

在act这个动作执行完毕之后,运行时系统首先会对日志进行验证,如果验证成功,就会提交(commit)日志。那么验证是怎么进行的呢?运行时系统会检查日志中缓存的每个readTVar的值是否与它们对应的真正的TVar相匹配。是的话便验证成功,并将日志中缓存的写操作结果全都提交到相应的TVar变量上。

必须注意的是,以上验证-提交的整个过程是完全不可分割的:底层实现会将中断禁止掉,或使用锁或CAS(compare-and-swap)指令等任何可行的方法来确保这个过程对于其他线程来说就像“一瞬间”的事情一样。但由于所有这些底层工作都由实现来完成,所以程序员不用担心也不用考虑它是怎么完成的。

一个自自而然的问题是:如果验证失败呢?如果验证失败,就代表该事务看到的是不一致的内存视图。于是事务被中止(abort),日志被重新初始化,然后整个事务从头再来过。这个过程就叫做重新执行(re-execution)。由于此时act动作的所有副作用都还没有真正提交到内存中,因此重新执行它是完全没问题的。然而有一点必须注意:act不能包含任何除了对TVar变量读写之外的副作用,比如下面这种情况:

       atomically (do { x <- readTVar xv                     ; y <- readTVar yv                     ; if x>y then launchMissiles                                 else return () })launchMissiles::IO ( )这个函数的副作用是“头晕、恶心、呕吐”。由于这个原子块执行的时候并没有加锁,所以如果同时有其他线程也在修改变量xv和yv的话,该线程就可能观察到不一致的内存视图。而一旦这种情况发生,发射导弹(launchMissiles)可就闯了大祸了,因为等到导弹发射完了才发现验证失败就已经来不及了。不过幸运的是,Haskell的类型系统会阻止冒失的程序员把IO动作(比如这个launchMissiles)放在STM动作中执行,所以,以上代码会被类型系统阻止。这从另一个方面显示了将IO动作跟STM动作区分开来的好处。
24.2.4 阻塞和选择
到目前为止我们介绍的原子块从根本上还缺乏一种能力:无法用来协调多个并发线程。这是因为还有两个关键的特性不具备:阻塞和选择。本节就来介绍如何扩充基本的STM接口从而使之包含以上两个特性(当然,在完全不破坏模块性的前提下)。

假设当一个线程试图从一个账户中提取超过账户余额的钱时这个线程便会阻塞。并发编程中这类情况很常见:例如一个线程在读取到一个空的缓冲区时阻塞;或在

参考文献编辑本段回目录

→如果您认为本词条还有待完善,请 编辑词条

词条内容仅供参考,如果您需要解决具体问题
(尤其在法律、医学等领域),建议您咨询相关领域专业人士。
0

标签: Simon Peyton Jones 西蒙·佩顿·琼斯

收藏到: Favorites  

同义词: Simon Peyton Jones

关于本词条的评论 (共0条)发表评论>>

对词条发表评论

评论长度最大为200个字符。