分布式编程框架 -第一部分(摘要)
构建分布式程序,
引言
软件的健壮性和可扩展性在很大程度上取决于过程式、逻辑式、功能式和物理式组件的分离,以及每个组件内层(tiers)的分离。
本文将介绍一种通过识别和管理功能分离来抽象顺序代码的方法,并构建一个编写分布式计算机程序的框架,使其无需担心其分布和执行的实现。
本文的第一部分将侧重于该框架背后的理论,而第二部分将侧重于该模式的实现。
背景
阅读以下内容可能有所帮助:
- n层架构
- 分布式计算
- 事件驱动架构
- 面向服务架构
- 多线程
- 过程式编程语言 (C#)
- 分布式共享内存 (DSM)
为什么要分布代码序列?
任务的分布需要从微观和宏观层面都有清晰的理解。为了形成更高阶任务而进行的抽象过程就需要这种理解。通过抽象任务,不仅可以通过引入分工来有效管理性能,还可以通过识别其内部关联性和依赖性来引入并行性。

如何做到?
最简单的形式,我们可以使用一个简单的数据结构,如 LinkedList<Action<I>>
,它存储了一系列代码块,唯一的联系是它们的执行顺序。然后我们可以通过迭代这个 LinkedList
从第一个 Action<I>
到最后一个来执行这个动作列表。假设没有依赖关系,这与 ThreadPool
相同。如果这是过程式动作,那么它就等同于过程式编程。这是琐碎且不有趣的。
一个稍微更有趣的场景是使用 LinkedList<Func<I,O>>
来管道化函数,其中一个函数的输出被管道化到下一个函数的输入。这种情况的先决条件是所有函数都有一个输入参数(类似于 **ML** 或 **F#**)。同样,这种情况无法比传统方法获得任何优势。
我们真正想要什么?
我们真正想要的是通过异步执行、多线程执行和在多个处理器上并行执行来利用分布式架构。正如我们所知,在这些领域进行编码在各个方面都可能极其困难。更重要的是,能够不必担心这些架构,而只是编写好的代码。
摘要
为了实现这个概念,我们的数据结构不能再是 LinkedList
,它暗示了执行顺序,而是我们需要明确定义每个函数的依赖关系,以获得更大的灵活性和管理性。
函数依赖
函数之间的依赖关系可以理解为这个规则:**给定函数 A 和 B,A 与 B 相关当且仅当 A 依赖于 B,或者 B 依赖于 A。**
我们可以使用集合来表示这种关系,如 {A, B, C | dp(B,A) ^ dp(B,C)}。这意味着给定一组函数 {A, B},B 依赖于 A 和 C。
示例
- 函数序列 <A, B, C> 可以看作是一组函数 {A, B, C},其中 B 依赖于 A,C 依赖于 B。 在这种情况下,C 依赖于 A 是隐含的。另外,由于 A 不依赖于任何其他函数,它可以被执行。*
- 给定一组函数 {A, B, C},其中 B 依赖于 A,C 依赖于 A 或 B。 在这种情况下,一旦 A 或 B 执行完毕,C 就可以执行。
- 给定一组函数 {A, B}。
没有依赖关系,这意味着 A 和 B 可以并行发生。先决条件是假设没有副作用。 - {A, B | dp(A,B) ^ dp(B,A)}。 这个程序的结果将永远不会被执行。
- {Stand, Walk, Look, Stop | dp(Walk,Stand) ^ (dp(Look,Stand) v dp(Look, Walk)) ^ dp(Stop, Walk)}。
程序将按如下方式运行:
Stand -> Walk -> Stop(注意:Stop 将在 Walk 或 Look 完成后运行,具体取决于哪个先完成。)
-> Look - {Stand, Walk, Look, Stop | dp(Walk,Stand) ^ (dp(Look,Stand) v dp(Look, Walk)) ^ (dp(Stop, Walk) ^ dp(Stop, Look))}.
程序将按如下方式运行:
Stand -> Walk
-> Look
-> Stop
不同函数之间依赖关系的概念尤其有用,因为它使我们能够将函数划分为独立的组。
(**注意**:请记住,我们只关心过程式分布,而不是功能式分布。在我们上面的示例 5 中,“Walk
”可能是一个持续 10 分钟的递归函数例程。)
过程式组
这样的一个组的定义是:**过程式组 G1 是一个函数序列 <x0, x1,...xn>,其中 x0 依赖于 yi,而 yi 不存在于 G1 中,并且 xn 依赖于 xn-1。**
示例
- {Stand, Walk, Look, Stop | dp(Walk,Stand) ^ (dp(Look,Stand) v dp(Look, Walk)) ^ dp(Stop, Walk)}。
G0 = <Stand, Walk, Stop>
G1 = <Look>
Stand -> Walk -> Stop
-> Look - {Stand, Walk, Look, Stop | dp(Walk,Stand) ^ (dp(Look,Stand) v dp(Look, Walk)) ^ (dp(Stop, Walk) ^ dp(Stop, Look))}.
G0 = <Stand, Walk>
G1 = <Look>
G2 = <Stop>Stand -> Walk
-> Look
-> Stop
根据定义,过程式组独立运行,从而允许我们为每个组创建单独的线程/处理器/计算机。这种分离转化为一个分布式的 n 层架构,可以同时执行以极大地提高整体性能。前提是我们利用了恰当的通信技术。
类型推断和参数
通信和数据传输是任何分布式系统的关键要素。为了确保信息流,我们需要建立严格的类型检查规则。类似于 ML,函数 A 的类型表示为 A:a'->b',其中 a' 是输入参数类型,b' 是函数 A 的输出类型。在这种情况下,由于我们不知道 A 的内部工作,仅从 A 来看,我们无法确定其输入和输出类型。因此 {A, B | dp(B,A)} => A:a'->b' 和 B:b'-c'。在这种情况下,A 的返回类型必须与 B 的输入类型相同。
示例
- {A, B, C | dp(C,A) ^ dp(C,B)} => A:a'->b' and B:c'->d' and C:(b',d')->e'
- {A, B, C | dp(C,A) v dp(C,B)} => A:a'->b' and B:c'->b' and C:b'->d'
在这种情况下,A 和 B 的返回类型必须相同。 - {A, B, C | dp(A,B) ^ (dp(C, B) v dp(C,A)) } => B:a'->b' and A:b'->b' and C:b'->c'
因为 C 依赖于 B 或 A,而 A 依赖于 B,所以 A 必须具有相同的输入和输出类型。
通过确保依赖函数的类型统一,我们现在可以将注意力转移到通信和信息传输上。
沟通
如今存在许多传输方法。COM+、MSQ、Remoting、Web Services 等都涉及面向服务的分布式架构和事件驱动架构。然而,这些都很好,但我们仍然需要配置和实现它们。BizTalk、SQL Server Enterprise 等产品能够在无需编写代码的情况下实现具有负载均衡、集群和并行处理的分布式计算。如果只有这种技术可用于普通编译器,它们可以生成可分发的代码集群,这些集群不仅可以执行函数代码,还可以根据服务器、处理器等的数量相互通信和同步。如果我们真的想这样做,我们可以利用 COM+ 和 MSQ 来处理传输层,但关键在于我们自己去研究它!
这是架构
(事件驱动的同步函数处理)
- 每个分布式单元(处理器、计算机)将充当一个代理。
- 所有代理都连接到一个服务总线。
- 有一个连接到服务总线的程序协调器。
- 协调器和代理彼此不知道对方,唯一的接口是服务总线。
事件
由于我们使用的是事件驱动架构,我们将需要为协调器和代理定义一些事件。
协调器
Distribute_Group
- **消息**:
Group
- **描述**:协调器发出堆栈中的下一个组。
代理
Request_Group
- **消息**:
null
- **描述**:代理当前没有处理任何组,因此它发出组请求。
- Processed_Function
- **消息**:
Function
- **描述**:代理已完成一个组的处理。
执行准备
协调器
- 每个函数都被分配了一个唯一的 ID。
IDictionary<int,Function> GenerateID(IEnumerable<Function>)
- 协调器确定程序的过程式组。
IEnumerable<Group> DistributeFunctions(IDictionary<int,Functions>)
- 每个组被分配一个唯一的 ID。
IDictionary<int,Group> GenerateID(IEnumerable<Group>)
代理
无需准备。
执行
协调器

- 等待
Request_Group
事件。 - 从字典中选择一个组。
- 发出
Distribute_Group
事件,将组作为其消息。
代理

- 如果代理是*空闲*的
- 发出
Request_Group
事件 - 等待
Distribute_Group
事件 - *拦截*事件
- 代理现在正在*处理组函数*
- 发出
- 如果代理正在*处理组函数*
- 对于组序列中的每个函数 f(x)
- 对于 f(x) 的每个依赖项 d(x)
- 等待 d(x) 的
Processed_Function
事件
- 等待 d(x) 的
- 使用事件消息处理 f(x)
- 发出
Processed_Function
事件,将 f(x) 的输出作为消息。
- 对于 f(x) 的每个依赖项 d(x)
- 代理现在*空闲*
- 对于组序列中的每个函数 f(x)
部署
由函数关系集合组成的 main assembly 被部署在协调器上。部署完成后,协调器将负责其余的事务。
随着越来越多的代理加入服务总线,整体性能将变得更快。
函数编译
为了让代理执行从协调器接收到的函数,每个 Group
都需要成为一个独立的程序集。这样,代理本身一开始就不需要包含任何代码,而是会在运行时接收不同的代码。我们将在本文的第二部分更深入地探讨这一点。
摘要
一旦我们清楚地分离了函数、它们的依赖关系、过程式组和通信,我们就可以构建一个真正能够分布式工作的程序了!
本文的第一部分涵盖了该框架的摘要。下一部分将介绍其实现。