BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

PDC 09:并行和异步编程中的挑战及F#的应对方案

| 作者 赵劼 关注 4 他的粉丝 发布于 2009年11月23日. 估计阅读时间: 8 分钟 | GMTC大前端的下一站,PWA、Web框架、Node等最新最热的大前端话题邀你一起共同探讨。

在最近举办的PDC 09大会中,F#的程序经理Luke Hoban发表了一场名为“F#并行与异步编程”的演讲,其中提出了并行与异步编程的多个重点与挑战,并解释了F#是如何从语言特性及类库框架两方面来给出合适应对方案。

F#是.NET平台上的又一门编程语言,结合了函数式编程及面向对象编程两种编程范式。微软研究院在5至6年开始着手设计并开发F#,并且将随Visual Studio 2010一起发布其稳定版本及相关工具包,可用于产品的开发。与C#和VB一样,F#是一门强类型的静态语言。Luke指出,F#是一门通用语言,可用于各种程序的开发,不过它的许多特性非常适用于开发那些算法性强,或是并行和异步占较大比重的应用程序。

Luke在演讲中提出了并行编程的四项挑战,其中第1个是“状态共享”。这里的共享状态特指可变(mutable)的状态,即可能被多个并行的组件所同时修改的内存。当遇到这样的情况,这意味着每次修改都可能影响多个组件,如果处理不当便可能造成难以预料的情况。而此时,从逻辑上分割各组件事实上也并非互相独立的,这给系统维护带来了困难。这种情况也很难测试,因为重现某个测试需要各并行的组件都处在特定的状况下。最大的问题可能是这样的代码很难高度并行,多个并行的组件如果需要共享同一块内存,则几乎一定会用到锁。锁很难处理,因为这非常依赖于各个组件是如何使用,以及何时使用某块内存的。如果程序新增了一个组件,甚至只是对现有组件做出少量修改,这可能就会让并行的应用程序对新的内存造成共享,于是便不知不觉地破坏了线程安全。

F#提出在语言特性上强化不可变的(immutable)程序开发方式。不可变的编程方式表示尽可能避免那些可修改的内存数据。在F#中,默认情况下的所有变量、函数、参数等等,一旦绑定(bind)至某个标识符后都是不可修改的。F#中的一些常用的数据类型,如Record,Tuple或Discriminated Union都是不可变的。如果想要一个状态不同的对象,开发人员只能“新建”而不能“修改”,而F#也提供了一定的语言特性来辅助此类操作。由于不可变性,在F#中便可以轻松使用各种方式进行并行计算,而不必担心线程安全问题。例如,可以使用.NET 4.0中的PLinq——在F#则被封装为PSeq模块进行序列的映射,过滤或求和等操作。此外,F#还提供了如List,Set,Map等不可变的常用数据结构。对于它的面向对象编程的部分,Luke指出F#也拥有一些特性,鼓励开发人员构建不可变的类型,即没有set操作,每个方法都只是根据参数进行计算并返回结果,而不是改变内部状态的类型。

Luke提出的第2个挑战是异步编程中的控制切换(Inversion of Control)问题。他认为,开发人员一直习惯于编写顺序的程序,即使用一行代码接着另一行代码的方式来实现逻辑。但是对于一些耗时很长的操作来说,这么做会阻塞程序的主线程,如在UI程序中阻塞主线程则会引起界面的僵死,此时往往需要异步调用。但是,异步调用需要将程序逻辑分为两个或多个阶段,在执行完一个阶段之后,再将结果通过回调函数传递给下一个。但编写这样的代码非常困难,往往需要为异步程序的控制编写大量代码,例如异常处理或任务取消等等。传统.NET异步编程模型,如解耦的Begin/End方法都无法解决这个问题。当需要异步调用的逻辑越来越多,甚至需要在其中加入一些循环或判断等逻辑,那程序的编写很容易变得越来越复杂。

F#中提供了一个名叫工作流(Workflow)的语言特性来应对这个问题。Workflow可以被认为是F#版本的monad实现,它的主要特色便是由编译器对顺序编写的代码进行desugar操作,形成回调的方式便于异步执行其中某些步骤。Luke演示了一个使用C#编写的,从Azure云中下载图片的WPF应用程序,其中长时间同步操作导致界面僵死。而将这段同步逻辑转化为异步则需要好几页的代码,其中的主要问题便是原本简单的for操作必须交由额外的上下文对象来保存,这样逻辑便在业务部分及异步控制部分中不断切换,造成难以实现和维护的代码。而使用F#实现相同的工作时,只需要使用async {...}将原有的逻辑包装起来,便形成了一个异步工作流。然后再将其中的一些耗时操作的let和do指令修改为let!或do!,这样便告知F#这两个步骤在执行时需要将控制权交还给框架,在得到结果之后才通过回调函数继续执行后面的逻辑。代码中原本的for循环可以被F#正确的处理,其表现形式和顺序的代码逻辑可谓毫无二致。值得一提的是,演示中Luke使用F#构建的类库可以直接被C#编写的WPF应用程序使用,唯一的修改只是引入了不同的命名空间而已。

第3个挑战是应用程序与I/O设备的交互,例如磁盘或是远程的云,这便是I/O密集型(I/O Bound)逻辑。由于各种I/O设备(如硬盘及网卡)往往是独立的,因此需要同时发起多个I/O请求才能够充分利用资源,提高程序的性能及响应能力。这便涉及到I/O并行(I/O Parallelism)。而使用async { ... }所形成的多个异步工作模块可以由F#组合成单个异步工作块,然后作为.NET 4.0中的任务(Task)执行。每个异步工作块中的I/O异步操作使用let!指令,在工作时可以将控制权交由F#,而保持原有逻辑的顺序性。由于每个I/O操作都是异步的,它并不会占用应用程序的工作线程。因此,即便是同时发起许多I/O请求,从任务管理器中也可以发现应用程序其实只使用了少量的线程。

最后一个挑战,是指并行应用程序往往只能简单实现向上扩展(Scale Up),而难以扩展至许多廉价机器所组成的集群。如果要有良好的向外扩展(Scale Out)能力,必须从程序设计初期便抱有这样的想法。这往往意味着使用消息和代理(agent)进行编程,它是一种为并行程序提供扩展能力的基础方式。Erlang及微软的Axum都使用了类似的思想,F#也提供了Agent组件,在每次发布过程中这个组件也在不断演化。使用基于Agent的方式,各组件的依赖便消失了,它们完全通过消息传递进行通信。F#的Agent组件是MailboxProcessor,它的Start函数会提供一个inbox。开发人员可以使用inbox的Receive方法发起一个非阻塞的接受操作,由于使用了异步工作块及let!指令,这行代码并不会阻塞线程,而是把控制权交由F#,直至获得一个消息。每个Agent对象都是非常轻量的对象,它与线程并没有对应关系。因此,即便是创建了大量的Agent对象也不会占用太多系统资源,F#会基于.NET 4.0中的TPL来合理并充分利用计算能力。

你可以在PDC 2009的网站上浏览或下载本次演讲的完整录像及幻灯片等资源。你也可以访问InfoQ中的F#栏目来获得更多相关内容。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT