`

一切为了并行:MS Axum语言教程 <一>

阅读更多

原文和更新参见: Ray Linn的EJB.CC
Axum 一个刚刚问世.net家族的新丁,几乎是和这篇文章一样新。它的诞生只为一个目的:一切为了并行。

编写并行程序并不容易,首先要将方案切割为许多并行任务(Task)。较为简单的方案,如计算每只股票的年收益率,很容易实施并行,因为任务之间彼此独立。但另外一些方案则十分复杂,任务之间彼此依赖且要相互协作,比如网络游戏。

Axum试图提供一种更为自然的方式来安排组件之间的协作问题。换句话说,如果将方案按照交互式组件建模,在Axum中编码会更为直接了当,而且你可能可以不被许多常见的并发问题所困扰。

并行的另一个目的是为了提速。经常,程序的反应速度总被象I/O或者用户输入这样的慢速组件所拖累。比如email客户端通过慢速的网络下载邮件的时候,程序还应该能继续响应用户的动作。

Axum的目标之一就是程序不需要再担心并发问题,理论上,你的程序将变得快速而且响应及时。为此,Axum不允许自由的共享和变更线程的状态,而只允许“有节制地”存取共享的状态,这解决了许多并发问题。

从"Hello,World"开始

Axum的安装包提供了Visual Studio的集成,你可以在Visual Studio 2008中使用Axum的解决方案。

我们从最简单的"Hello,World"入手。
using System; 
agent Program : Microsoft.Axum.ConsoleApplication { 
  override int Run(String[] args) { 
     Console.WriteLine("Hello, World!.....MS Axum By Ray_Linn!"); 
  } 
}


这段程序以关键字“agent”开始,Axum中“代理”的概念来自模式中常见的“参与者模式”:参与者被描述成自治的实体,互相之间则通过消息来沟通,处理它们从其他参与者接受的数据,并发送给其他参与者。Axum中参与者表现为“代理”. 用Axum编写程序都是在定义代理并安排他们之间的交互。在许多方面,基于代理编程都不同于面向对象编程。首先不同于对象,代理不提供公共方法或展示他们的状态。你不能“进入”代理修改任何字段。你不能调用代理的方法并等它给你结果。相反地,你可以给它发送消息,要求它完成之后给你个响应。


Axum提供了一些支持类库包括"ConsoleApplication"等代理,这个代理完成了控制台程序”所需的一些工作:启动,设置命令行参数,停止。

上面的程序中的代理衍生自ConsoleApplication,因此我们重写(Override)入口点的Run方法。因为Axum是.net家族中的一员,它可以分享已有的所有类库。比如程序中的System.Console.WriteLine就来自基础类库BCL.

消息流动 -Message-Passing

“消息流动”这个词是我杜撰的,因为它很形象地说明了Axum中的类似流水的消息处理方式,这里我们要介绍两个概念channel-通道和通道的端口port。

代理就像是个水处理机一样,它是提供数据处理的组件,它也有进水口和出水口--我们称之为通道(channel),不同的数据则从不同的通道端口(channel port)流经代理。先看个简单的例子

using System; 
agent Program : channel Microsoft.Axum.Application { 
  public Program() { 
  // Receive command line arguments from port CommandLine String []   
  args = receive(PrimaryChannel::CommandLine); 
  // Send a message to port ExitCode: 
  PrimaryChannel::ExitCode <-- 0; 
  } 
}


代理"Program"实现了一个通道Microsoft.Axum.Application。 从语法和语义上看,实现通道与继承父代理是截然不同的。一个代理继承另一个代理时,我们只要重写其中的一些虚方法,并添加自己另外一些方法即可。

而当一个代理实现通道之后(注意关键字"channel"紧跟在代理声明的冒号之后),它就和水管一样,一头连在进水端,我们通常叫"Implementing End",另一段连在出水端 - "Using End",连在其后的其他组件就成为了客户端。

通道的存在就是为了定义和传输数据而不提供任何处理,数据的处理由代理来完成。

例子中的Application Channel的"Using End"是由Axum运行时实现的。运行时实例化了一个实现了Microsoft.Axum.Application的代理,并复杂将命令行参数发送至通道的CommandLine端口,然后监听ExitCode端口,收到消息的时候,程序就退出了。

而我们的代理“Program"则在CommandLine端口上等待消息的到来,并负责发送完成信号到ExitCode端口(操作符 <--)。Program代理内建了属性PrimaryChannel来存取所实现的通道,有时候我们称它为“代理的主通道”。双冒号“::”则用来存取通道的端口。

请注意上面提到的“等待消息”。Axum中,接收消息是阻塞操作,这意味着从空端口读取数据的动作将延迟到消息被送抵。另一方面消息发送是异步的,消息发送者不需要一直等待消息被送至目的地。

消息的异步编程

随着消息成为代理之间的主要通讯手段,现在我们需要一些系统的方法来处理它们。总体上,我们把这称为“编曲-orchestration”.Axum提供了两种截然不同的编曲方法:基于控制流与基于数据流。通常,二者被联合使用以实现最后的效果。

在Axum中,消息在交互点之间被发送和接收。消息起源的交互点被称为“源头”,消息的目的地则被称为“目标”。一个交互点可同时成为源头与目标,这意味着,它可以同时发送和接受消息。许多交互点可以连接成数据流网。

简单的说,数据流网构建了数据的转换过程,并产生结果。如果网络上的一些节点间相互依赖,使用数据流网是有利的,由此可并发执行。

不同的是,控制流逻辑是基于条件表达式,循环,方法调用这些逻辑,数据流网则基于在网络中传递、过滤、广播、负载平衡和加入消息的逻辑。请看一个斐波纳契数列的例子:
agent MainAgent : channel Microsoft.Axum.Application { 
   function int Fibonacci(int n) {
        if( n<=1 ) return n;
        return Fibonacci(n-1) + Fibonacci(n-2);
    }
    int numCount = 10;
    
    void ProcessResult(int n)
    {
        Console.WriteLine("Ray_Linn's Fibonacci: "+ n);
        if( --numCount == 0 )
        PrimaryChannel::ExitCode <-- 0;
    }
    
    public MainAgent()
    {
        var numbers = new OrderedInteractionPoint<int>();
        // Create pipeline: 
        numbers ==> Fibonacci ==> ProcessResult;
        // Send messages to numbers:
        for( int i=0; i<numCount; i++ )
        numbers <-- 42-i;
    } 
}

函数“Fibonacci”带有关键字“function",在Axum中,function意味着函数不修改它自身之外的任何状态,换句话说,它的执行不会有任何副作用。如果你试图修改numCount,或者想在“Fibonacci”函数中发送消息,编译器就会报错。

接着看下MainAgent的构造器。第一行创建了OrderedInteractionPoint<int>的实例,它是一个同时扮演“源头”与“目标”的交互点。“ordered”意味着消息接受和发送的顺序是一致的。

接下来,代理创建了用消息传递操作符 “==>”构造了数据流网。
numbers ==> Fibonacci ==> PrintResult;


这个表达式应该被理解为“当一个消息到达交互点“number”时,将它传递到由“Fibonacci”实现的转换交互点,最后传递结果到PrintResult方法”。

将消息从一个节点传递至另一个节点的数据流网相当普遍的,它有一个名称“管道”。

由于Fibonacci函数没有副作用,Axum运行时会产生能让程序最高效运行所需的线程,以便并发执行转换工作。

函数经常被用在管道和其他类型网络中的并发执行节点上。


版权所有,如果转载,请站内联系。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics