Chinaunix首页 | 论坛 | 博客
  • 博客访问: 14497840
  • 博文数量: 5645
  • 博客积分: 9880
  • 博客等级: 中将
  • 技术积分: 68081
  • 用 户 组: 普通用户
  • 注册时间: 2008-04-28 13:35
文章分类

全部博文(5645)

文章存档

2008年(5645)

我的朋友

分类:

2008-04-28 21:34:51

下载本文示例代码
                推荐:经典教程专区  深远影响   在过去30年里,并发虽然一直被鼓吹为“下一件大事”或“未来之路”,但软件界不为所动。现在,并行终于出现在我们面前了:新一代计算机全面支持并发,这将引发软件开发方式的巨变。   本文主要讨论并发对软件——包括编程语言和程序员——的深远影响。   Olukotun和Hammond所描述的硬件发展,代表着计算机计算方式的重大变化。过去30年里,半导体工业的发展及其在处理器上的应用,推动了已有顺序式软件运行速度的稳步提升。但体系结构上的多核处理器变化仅对并发应用有益,因此几乎对绝大多数现存软件没有价值。在不久的将来,已有的桌面应用不可能比现在跑得更快。实际上,它们的运行速度会稍慢,因为为了降低高密度多核处理器的电能消耗,新的芯片内核被简化并运行在更低的时钟速度。   这将对软件至少是主流软件的开发带来深远影响。计算机的能力无疑越来越强,但程序不再可能从硬件性能提升大餐中免费获益,除非它们实现了并发。   即便抛开多核变化的强制要求不谈,我们也有理由实现并发,尤其是将工作从同步转到异步,可以提高响应速度,就像目前的应用里必须让工作远离GUI线程,以便计算在后台进行时,屏幕能得到重绘。   但实现并发是有难度的。不仅目前的语言和工具仍未为将应用转化为并行程序做好充分准备,而且在主流应用中也很难找到并行,尤其糟糕的是——并发要求程序员以人类难以适应的方式思维。   不过,多核在未来不可回避,我们必须找到与之相适应的软件开发方式。接下来,我们将深入探讨并发的难度所在,以及一些未来可能的应对方向。   软件新纪元   今天的并发编程语言和工具几乎与结构化编程时代初期的顺序化编程同时起步。信号量和协程是实现并发的基本手段,锁与线程建立在更高层次,可以结构化构造并发程序。而我们需要的是以面向对象为基础的并发——建立在更高层次的抽象有助于构建并发程序,就像面向对象抽象有益于构建大型组件化程序一样。   几个因素决定了并发变革对我们冲击可能比面向对象大。首先,并发是获得更高性能的必需手段。像C之类的语言,无视面向对象,但仍能在很多开发领域发挥作用。如果并发变成了应用性能提升的华山一条路,那么商业和系统语言的存在价值就只能建立在支持并发编程的基础上。因此现存的如C之类的语言,就必须支持超越pthreads之流简单模式以外的并发特性,不能支持并发编程的语言将逐步走向消亡,而仅仅能在现代硬件不重要的场合占得一席之地。   并发比面向对象冲击更大的第二个原因是,尽管顺序化编程已经很难,但并发编程更难。例如,在分析顺序化程序时,环境相关性分析是考量环境影响行为的基础技术。并发程序则还需要进行同步分析,而同时做环境相关性和同步分析已经被证明不可企及   最后,人类在遭受并发的突然袭击后,发现并发程序比顺序化代码难以把握得多。即使最细心的程序员,也很可能考虑不到简单半有序作业集里的交叉问题。   客户端和服务端应用的差别   对客户端应用来说,并发是一个挑战。但是在很多服务端程序里,并发则是一个“已经解决的问题”,我们可以例行公事般地构造并发应用,结果它就能很好工作,尽管进一步改进程序以确保其更具扩展性,仍需艰巨努力。这些程序通常都包含大量并行,因为它们同时处理的是大量的彼此无关的请求。比如Web服务器和网站,运行的是同样代码的副本,处理绝大多数时候彼此无关的数据。   而且,它们的执行环境被隔离,通过高度支持并发存取结构化数据的数据库之类的抽象数据访问方式实现状态共享。因此,代码通过数据库实现数据共享,就能得到“安宁从容的感觉”——就好像运行在一个整洁、单线程的世界一样。   而客户端应用的世界里,则没有那么规则和结构化。通常,客户端程序为单用户执行相关的小型运算,由此出发,我们发现可以通过将运算分割为多个更有效率的小片断来实现并发。也就是说让用户界面和程序运算小片断可能以多种方式交互和共享数据。但这类程序难以并发执行,因为其代码是非均态的,结构密织、交互复杂,而且操作的是基于指针的数据结构。   并行   ·编程模型(Programming Models)   现在,你能用多种方式实现并行,但每种方式仅仅适用于特定类型程序。大多数时候,没有细致入微的设计与分析,很难事先知道哪种模型适合给定问题。如果无法清楚确定给出的问题能套用哪种模型,往往就必须将多个模型杂合起来灵活运用。   这些并行编程模型可以从以下两个方面明确加以区分:并行作业的粒度,和任务间的耦合程度。这两个方面的不同,造就了迥异的编程模型。我们接下来依次讨论。   并行作业,小到单个指令,比如加法和乘法运算,大到需花费数小时甚至数天的程序。显然,并行体系中小作业的累计耗费是巨大的,因此,像并行指令运算等一般要求用硬件实现。与多处理器结构相比,多核处理器减少了通讯和同步消耗,因此减少了小片代码的累计成本。同样,一般来说,粒度划分越小,就越要注意拆分任务、以及为它们提供彼此间通讯和同步的成本。   另一方面是作业在通讯和同步时的耦合程度。不要有这样的幻想:作业完全独立执行,最后产生完全不同的输出。在这种情况下,各作业有序执行,不会引发同步和通讯问题,很容易实现无数据竞争可能的编程。这样的情况是罕见的,绝大多数程序的并发作业都要共享数据。因此作业更为变化多端,保证其正确和高效的复杂性也大为增加。最简单的情况是每个任务执行完全一样的代码。这种类型的共享常常是规则的,通过对单个任务的分析就能理解。更具挑战性的是不规则并行,这个时候,各作业的情况互不相同,共享模式更难于理解。   ·无依赖并行(Independent parallelism)   可能最简单、只具有基本行为的模型是无依赖并行(有时候也叫密集并行(EP,Embarrassingly Parallel)),一个或者多个作业独立运行,处理分离的数据。   小粒度数据并行依赖于并发执行的作业的独立性。它们应该无输入输出数据的共享、无交叉执行,比如:   double A[100][100];   …   A = A * 2;   求得100×100数组每个元素与2的乘积,然后保存到原元素位置。100000次的乘法运算都独立运行,彼此没有交叉。它可能是比大多数计算机的实际需要更理想化的并行模型,粒度很小,因此实际应用中通常会将矩阵分成n×m个块,然后在这些块的基础上执行并行计算。   而在粒度轴的另一端,很多应用,比如搜索引擎,共享仅仅一个巨型只读数据库,因此要求并行查询没有交叉。同样,大规模仿真系统要求很多并行程序同时访问包含输入参数的大型空间,这是一个让人颇感棘手的并行应用。   ·规则并行(Regular parallelism)   比无依赖并行略为复杂的是计算工作彼此依赖时,将同样的操作应用到一个数据集上。如果在两个操作间需要通讯或同步,则操作间存在依赖。   我们考虑以下用四个相邻元素的平均值替换数组中每个元素的运算模型:   A[i, j] = (A[i-1, j] A[i, j-1] A[i 1, j] A[i, j 1]) / 4;   这类运算要求细致协调,确保在用平均值替换前,已经准备好数组中相邻数据。如果不考虑空间消耗,可以将计算得到的平均值写入一个新数组。一般,其他更结构化的计算策略,比如用Diagonal Wavefront算法访问数组,会得到同样的结果,而且有更好的缓存寻址和更低的内存消耗收益。   规则并行程序可能需要同步控制,或者精心排练的执行策略,以确保得到正确的结果,但不像普通并行,它可以通过分析隐藏在操作后面的代码以确定怎样实现并行,并知道如何共享数据。   再次移到粒度轴的另一端,譬如Web站点上的运算工作,除了访问相同数据库,存在典型的独立性。因此除了数据库事务,所有的运算都并行进行,不需要大量协调工作。   ·无结构并行(Unstructured parallelism)   大多数情况下,最没有规律的并行形式是并行运算彼此不同,因此它们的数据访问无可预测,需要通过显式同步加以协调。这是使用多线程和显式同步编写的程序里最常见的并行形式,其中的线程在程序里扮演着互不相同的角色。一般,对于这种并行形式,除了知道两个线程访问数据发生冲突时需要显式同步,我们在其他方面很难说出个头头道道;另外,这类程序具有不确定性。   锁   ·状态共享存在的问题;锁的局限性   无结构并行面临的又一个挑战来源于无结构状态共享。客户端应用通常通过共享内存的办法来解决对象图(Graphs of Objects)中交互行为无可预测的问题。   假设两个任务希望访问同一个对象,其中一个可以修改对象的状态,如果我们不加控制,那么就会产生数据竞争。竞争的后果很糟糕,并发任务可能读写到不一致或已损毁的数据。   有大量同步策略可以解决数据竞争问题,其中最简单的就是锁。每一个需要访问共享数据片的任务在访问数据前必须申请得到锁,然后执行计算;最后要释放锁,以便其他任务可以对这些数据执行别的操作。不幸的是,尽管锁在一定程度上能避免数据竞争,但它也给现代软件开发带来了严重问题。   最主要的问题是,锁不具有可组合性。你不能保证由两部分以锁为基础、能正确运行的代码合并得到的程序依然正确。而现代软件开发的基础恰恰是将小程序库合并为更大程序的组装能力;因此,我们无法做到不考察组件的具体实现,就能在它们基础上组装大软件,这是个大问题。   造成组装失败的祸首是死锁。考虑最简单的情况,当两个任务以相反顺序申请两个锁时,死锁就出现了:任务T1获得了锁L1,任务T2获得了锁L2,然后,T1申请获得锁L2,同时T2申请获得L1。此时,两个任务将永久阻塞。而被以相反顺序请求两个锁的情况在任何时候都可能发生,所以获得一个锁后,只有让调用进入你无法控制代码的内部,才可能找到解决死锁问题的办法。   但是,那些可扩展的框架却没有考虑这个问题。即使目前最根正苗红的商业应用框架也是这么干的,包括.NET框架、Java标准库等。之所以没出什么大问题,是因为开发人员仍然没有编写要求频繁锁定的大量并发的程序。很多复杂模型希望解决死锁问题——比如实现退避/重送(backoff-and-retry)协议——但这些模型都需要程序员经受严格训练,并且一些解决办法还可能引入其他问题(比如活锁(或空转,livelock))。   通过确保所有锁申请都只能在安全顺序基础上得到满足的死锁预防技术,也无能为力。例如锁调整与锁分级策略,要求所有同级锁按照预定顺序一次满足,此后只能申请获得更高级别的单一锁,以此的确可以避免锁冲突。这类技术虽然在实践中还未普及,不过在单一团队维护的模块和框架内已经发挥了作用。但是,它们要求对全部代码的完全控制。这就严重限制了这些技术在可扩展框架、插件系统和其他需要将多方编写的代码整合环境里的应用。   一个更基本问题是,锁的实现依赖于程序员对协定的严格遵循。锁与它所保护的数据间关系相当隐讳,只能通过程序员的纪律性得到维系。程序员必须总能清楚记得访问共享数据前,要在正确地点放置恰当的锁。有时,程序里的锁管理协定的确存在,但它们几乎从来没有精确到可供工具实现检验。   锁还有其他一些更加微妙的问题。锁定是一个全局属性,很难被本地化到单个过程、类或者框架。所有访问共享数据片的代码都必须知道且服从锁约定,无论是谁编写这些代码,代码用在什么地方。   即便将同步本地化,也并非任何时候都能正常工作。拿一个常见解决方案,如Java中的synchronized方法来说。对象的每个方法都能从对象得到一个锁,所以没有任何两个线程可以同时直接访问对象的状态。而对象状态仅能通过对象的方法访问,程序员又记得给方法增加了synchronized声明,如此一来,看似达到了我们的目的。   而实际上,synchronized方法至少有三个主要的问题。首先,它们不适合于其方法会调用别的对象(比如Java的Vector和.NET的SyncHashTable)的虚函数的类型,因为获得一个锁后调用进入第三方代码,就可能引发死锁。其次,synchronized方法可能导致频繁锁定,因为它们要求在所有的对象实例上获得和释放锁,即便很多对象从不存在线程交互。第三,synchronized方法的锁粒度过小,当程序在单或多个对象上调用多个方法时,无法保证操作的原子性。我们以如下简单的银行事务为例:   account1.Credit(amount); account2.Debit(amount)   逐对象(Per-object)锁定可以保护每个调用,但不能阻止其他线程看到两个账户在多次调用之间的不一致信息。这种类型的操作,原子性与单个调用的边界不吻合,因此需要额外的、显式同步。   ·锁的替代者   考虑到内容完整性,我得提一下锁的两个主要备选方案。一个是无锁编程(Lock-Free Programming)。通过对处理器内存模式的深入认识,建立可共享但无显式锁定的数据结构是可能的。无锁编程难度很大且非常脆弱,因此新的无锁数据结构实现方案,仍在不断修改之中。   第二个替代物是事务内存(Transactional Memory),它将数据库中事务模式的核心思想移植到编程语言里来。程序员将自己的程序编写为一个个具有确定原子性的块,它们可以分离执行,因此只有在每个原子行为执行前和执行后,并发操作才能访问共享数据。尽管很多人看好事务内存的前途,但它目前仍处于研究之中。 共2页。 1 2 :                 推荐:经典教程专区  深远影响   在过去30年里,并发虽然一直被鼓吹为“下一件大事”或“未来之路”,但软件界不为所动。现在,并行终于出现在我们面前了:新一代计算机全面支持并发,这将引发软件开发方式的巨变。   本文主要讨论并发对软件——包括编程语言和程序员——的深远影响。   Olukotun和Hammond所描述的硬件发展,代表着计算机计算方式的重大变化。过去30年里,半导体工业的发展及其在处理器上的应用,推动了已有顺序式软件运行速度的稳步提升。但体系结构上的多核处理器变化仅对并发应用有益,因此几乎对绝大多数现存软件没有价值。在不久的将来,已有的桌面应用不可能比现在跑得更快。实际上,它们的运行速度会稍慢,因为为了降低高密度多核处理器的电能消耗,新的芯片内核被简化并运行在更低的时钟速度。   这将对软件至少是主流软件的开发带来深远影响。计算机的能力无疑越来越强,但程序不再可能从硬件性能提升大餐中免费获益,除非它们实现了并发。   即便抛开多核变化的强制要求不谈,我们也有理由实现并发,尤其是将工作从同步转到异步,可以提高响应速度,就像目前的应用里必须让工作远离GUI线程,以便计算在后台进行时,屏幕能得到重绘。   但实现并发是有难度的。不仅目前的语言和工具仍未为将应用转化为并行程序做好充分准备,而且在主流应用中也很难找到并行,尤其糟糕的是——并发要求程序员以人类难以适应的方式思维。   不过,多核在未来不可回避,我们必须找到与之相适应的软件开发方式。接下来,我们将深入探讨并发的难度所在,以及一些未来可能的应对方向。   软件新纪元   今天的并发编程语言和工具几乎与结构化编程时代初期的顺序化编程同时起步。信号量和协程是实现并发的基本手段,锁与线程建立在更高层次,可以结构化构造并发程序。而我们需要的是以面向对象为基础的并发——建立在更高层次的抽象有助于构建并发程序,就像面向对象抽象有益于构建大型组件化程序一样。   几个因素决定了并发变革对我们冲击可能比面向对象大。首先,并发是获得更高性能的必需手段。像C之类的语言,无视面向对象,但仍能在很多开发领域发挥作用。如果并发变成了应用性能提升的华山一条路,那么商业和系统语言的存在价值就只能建立在支持并发编程的基础上。因此现存的如C之类的语言,就必须支持超越pthreads之流简单模式以外的并发特性,不能支持并发编程的语言将逐步走向消亡,而仅仅能在现代硬件不重要的场合占得一席之地。   并发比面向对象冲击更大的第二个原因是,尽管顺序化编程已经很难,但并发编程更难。例如,在分析顺序化程序时,环境相关性分析是考量环境影响行为的基础技术。并发程序则还需要进行同步分析,而同时做环境相关性和同步分析已经被证明不可企及   最后,人类在遭受并发的突然袭击后,发现并发程序比顺序化代码难以把握得多。即使最细心的程序员,也很可能考虑不到简单半有序作业集里的交叉问题。   客户端和服务端应用的差别   对客户端应用来说,并发是一个挑战。但是在很多服务端程序里,并发则是一个“已经解决的问题”,我们可以例行公事般地构造并发应用,结果它就能很好工作,尽管进一步改进程序以确保其更具扩展性,仍需艰巨努力。这些程序通常都包含大量并行,因为它们同时处理的是大量的彼此无关的请求。比如Web服务器和网站,运行的是同样代码的副本,处理绝大多数时候彼此无关的数据。   而且,它们的执行环境被隔离,通过高度支持并发存取结构化数据的数据库之类的抽象数据访问方式实现状态共享。因此,代码通过数据库实现数据共享,就能得到“安宁从容的感觉”——就好像运行在一个整洁、单线程的世界一样。   而客户端应用的世界里,则没有那么规则和结构化。通常,客户端程序为单用户执行相关的小型运算,由此出发,我们发现可以通过将运算分割为多个更有效率的小片断来实现并发。也就是说让用户界面和程序运算小片断可能以多种方式交互和共享数据。但这类程序难以并发执行,因为其代码是非均态的,结构密织、交互复杂,而且操作的是基于指针的数据结构。   并行   ·编程模型(Programming Models)   现在,你能用多种方式实现并行,但每种方式仅仅适用于特定类型程序。大多数时候,没有细致入微的设计与分析,很难事先知道哪种模型适合给定问题。如果无法清楚确定给出的问题能套用哪种模型,往往就必须将多个模型杂合起来灵活运用。   这些并行编程模型可以从以下两个方面明确加以区分:并行作业的粒度,和任务间的耦合程度。这两个方面的不同,造就了迥异的编程模型。我们接下来依次讨论。   并行作业,小到单个指令,比如加法和乘法运算,大到需花费数小时甚至数天的程序。显然,并行体系中小作业的累计耗费是巨大的,因此,像并行指令运算等一般要求用硬件实现。与多处理器结构相比,多核处理器减少了通讯和同步消耗,因此减少了小片代码的累计成本。同样,一般来说,粒度划分越小,就越要注意拆分任务、以及为它们提供彼此间通讯和同步的成本。   另一方面是作业在通讯和同步时的耦合程度。不要有这样的幻想:作业完全独立执行,最后产生完全不同的输出。在这种情况下,各作业有序执行,不会引发同步和通讯问题,很容易实现无数据竞争可能的编程。这样的情况是罕见的,绝大多数程序的并发作业都要共享数据。因此作业更为变化多端,保证其正确和高效的复杂性也大为增加。最简单的情况是每个任务执行完全一样的代码。这种类型的共享常常是规则的,通过对单个任务的分析就能理解。更具挑战性的是不规则并行,这个时候,各作业的情况互不相同,共享模式更难于理解。   ·无依赖并行(Independent parallelism)   可能最简单、只具有基本行为的模型是无依赖并行(有时候也叫密集并行(EP,Embarrassingly Parallel)),一个或者多个作业独立运行,处理分离的数据。   小粒度数据并行依赖于并发执行的作业的独立性。它们应该无输入输出数据的共享、无交叉执行,比如:   double A[100][100];   …   A = A * 2;   求得100×100数组每个元素与2的乘积,然后保存到原元素位置。100000次的乘法运算都独立运行,彼此没有交叉。它可能是比大多数计算机的实际需要更理想化的并行模型,粒度很小,因此实际应用中通常会将矩阵分成n×m个块,然后在这些块的基础上执行并行计算。   而在粒度轴的另一端,很多应用,比如搜索引擎,共享仅仅一个巨型只读数据库,因此要求并行查询没有交叉。同样,大规模仿真系统要求很多并行程序同时访问包含输入参数的大型空间,这是一个让人颇感棘手的并行应用。   ·规则并行(Regular parallelism)   比无依赖并行略为复杂的是计算工作彼此依赖时,将同样的操作应用到一个数据集上。如果在两个操作间需要通讯或同步,则操作间存在依赖。   我们考虑以下用四个相邻元素的平均值替换数组中每个元素的运算模型:   A[i, j] = (A[i-1, j] A[i, j-1] A[i 1, j] A[i, j 1]) / 4;   这类运算要求细致协调,确保在用平均值替换前,已经准备好数组中相邻数据。如果不考虑空间消耗,可以将计算得到的平均值写入一个新数组。一般,其他更结构化的计算策略,比如用Diagonal Wavefront算法访问数组,会得到同样的结果,而且有更好的缓存寻址和更低的内存消耗收益。   规则并行程序可能需要同步控制,或者精心排练的执行策略,以确保得到正确的结果,但不像普通并行,它可以通过分析隐藏在操作后面的代码以确定怎样实现并行,并知道如何共享数据。   再次移到粒度轴的另一端,譬如Web站点上的运算工作,除了访问相同数据库,存在典型的独立性。因此除了数据库事务,所有的运算都并行进行,不需要大量协调工作。   ·无结构并行(Unstructured parallelism)   大多数情况下,最没有规律的并行形式是并行运算彼此不同,因此它们的数据访问无可预测,需要通过显式同步加以协调。这是使用多线程和显式同步编写的程序里最常见的并行形式,其中的线程在程序里扮演着互不相同的角色。一般,对于这种并行形式,除了知道两个线程访问数据发生冲突时需要显式同步,我们在其他方面很难说出个头头道道;另外,这类程序具有不确定性。   锁   ·状态共享存在的问题;锁的局限性   无结构并行面临的又一个挑战来源于无结构状态共享。客户端应用通常通过共享内存的办法来解决对象图(Graphs of Objects)中交互行为无可预测的问题。   假设两个任务希望访问同一个对象,其中一个可以修改对象的状态,如果我们不加控制,那么就会产生数据竞争。竞争的后果很糟糕,并发任务可能读写到不一致或已损毁的数据。   有大量同步策略可以解决数据竞争问题,其中最简单的就是锁。每一个需要访问共享数据片的任务在访问数据前必须申请得到锁,然后执行计算;最后要释放锁,以便其他任务可以对这些数据执行别的操作。不幸的是,尽管锁在一定程度上能避免数据竞争,但它也给现代软件开发带来了严重问题。   最主要的问题是,锁不具有可组合性。你不能保证由两部分以锁为基础、能正确运行的代码合并得到的程序依然正确。而现代软件开发的基础恰恰是将小程序库合并为更大程序的组装能力;因此,我们无法做到不考察组件的具体实现,就能在它们基础上组装大软件,这是个大问题。   造成组装失败的祸首是死锁。考虑最简单的情况,当两个任务以相反顺序申请两个锁时,死锁就出现了:任务T1获得了锁L1,任务T2获得了锁L2,然后,T1申请获得锁L2,同时T2申请获得L1。此时,两个任务将永久阻塞。而被以相反顺序请求两个锁的情况在任何时候都可能发生,所以获得一个锁后,只有让调用进入你无法控制代码的内部,才可能找到解决死锁问题的办法。   但是,那些可扩展的框架却没有考虑这个问题。即使目前最根正苗红的商业应用框架也是这么干的,包括.NET框架、Java标准库等。之所以没出什么大问题,是因为开发人员仍然没有编写要求频繁锁定的大量并发的程序。很多复杂模型希望解决死锁问题——比如实现退避/重送(backoff-and-retry)协议——但这些模型都需要程序员经受严格训练,并且一些解决办法还可能引入其他问题(比如活锁(或空转,livelock))。   通过确保所有锁申请都只能在安全顺序基础上得到满足的死锁预防技术,也无能为力。例如锁调整与锁分级策略,要求所有同级锁按照预定顺序一次满足,此后只能申请获得更高级别的单一锁,以此的确可以避免锁冲突。这类技术虽然在实践中还未普及,不过在单一团队维护的模块和框架内已经发挥了作用。但是,它们要求对全部代码的完全控制。这就严重限制了这些技术在可扩展框架、插件系统和其他需要将多方编写的代码整合环境里的应用。   一个更基本问题是,锁的实现依赖于程序员对协定的严格遵循。锁与它所保护的数据间关系相当隐讳,只能通过程序员的纪律性得到维系。程序员必须总能清楚记得访问共享数据前,要在正确地点放置恰当的锁。有时,程序里的锁管理协定的确存在,但它们几乎从来没有精确到可供工具实现检验。   锁还有其他一些更加微妙的问题。锁定是一个全局属性,很难被本地化到单个过程、类或者框架。所有访问共享数据片的代码都必须知道且服从锁约定,无论是谁编写这些代码,代码用在什么地方。   即便将同步本地化,也并非任何时候都能正常工作。拿一个常见解决方案,如Java中的synchronized方法来说。对象的每个方法都能从对象得到一个锁,所以没有任何两个线程可以同时直接访问对象的状态。而对象状态仅能通过对象的方法访问,程序员又记得给方法增加了synchronized声明,如此一来,看似达到了我们的目的。   而实际上,synchronized方法至少有三个主要的问题。首先,它们不适合于其方法会调用别的对象(比如Java的Vector和.NET的SyncHashTable)的虚函数的类型,因为获得一个锁后调用进入第三方代码,就可能引发死锁。其次,synchronized方法可能导致频繁锁定,因为它们要求在所有的对象实例上获得和释放锁,即便很多对象从不存在线程交互。第三,synchronized方法的锁粒度过小,当程序在单或多个对象上调用多个方法时,无法保证操作的原子性。我们以如下简单的银行事务为例:   account1.Credit(amount); account2.Debit(amount)   逐对象(Per-object)锁定可以保护每个调用,但不能阻止其他线程看到两个账户在多次调用之间的不一致信息。这种类型的操作,原子性与单个调用的边界不吻合,因此需要额外的、显式同步。   ·锁的替代者   考虑到内容完整性,我得提一下锁的两个主要备选方案。一个是无锁编程(Lock-Free Programming)。通过对处理器内存模式的深入认识,建立可共享但无显式锁定的数据结构是可能的。无锁编程难度很大且非常脆弱,因此新的无锁数据结构实现方案,仍在不断修改之中。   第二个替代物是事务内存(Transactional Memory),它将数据库中事务模式的核心思想移植到编程语言里来。程序员将自己的程序编写为一个个具有确定原子性的块,它们可以分离执行,因此只有在每个原子行为执行前和执行后,并发操作才能访问共享数据。尽管很多人看好事务内存的前途,但它目前仍处于研究之中。 共2页。 1 2 : 下载本文示例代码


软件与并发巨变 不得不面对的革命软件与并发巨变 不得不面对的革命软件与并发巨变 不得不面对的革命软件与并发巨变 不得不面对的革命软件与并发巨变 不得不面对的革命软件与并发巨变 不得不面对的革命软件与并发巨变 不得不面对的革命软件与并发巨变 不得不面对的革命软件与并发巨变 不得不面对的革命软件与并发巨变 不得不面对的革命软件与并发巨变 不得不面对的革命软件与并发巨变 不得不面对的革命软件与并发巨变 不得不面对的革命软件与并发巨变 不得不面对的革命软件与并发巨变 不得不面对的革命
阅读(125) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~