11分钟阅读

酏剂和OTP中的过程面向过程编程指南

Michael是一名具有深层技术和领导技能的软件工程师。作为企业家,他都是领导者和团队球员。

人们喜欢将编程语言分类为范式。有面向对象(OO)语言,命令语言,功能语言等,这有助于解决哪些语言解决类似问题,语言旨在解决哪些类型的问题。

在每种情况下,范式通常都有一个“主要的”焦点和技术,即该族语言的驱动力:

  • 在OO语言中,它是 类或对象 作为一种封装状态(数据)的方式,操纵该状态(方法)。

  • 在功能语言中,它可以是 操纵功能 他们自己或者 不可变数据 从功能传递到功能。

尽管 酏剂 (和 erlang 之前)通常被归类为功能语言,因为它们表现出对功能语言共同的不变数据,我会提交它们代表一个 单独的范例免受许多功能语言的。它们存在并通过ETP的存在而被采用,因此我会将其分类为 以过程为导向的语言.

在这篇文章中,我们将捕获面向过程的编程的含义是使用这些语言时,探索其他范式的差异和相似性,看看培训和采用的影响,并以短期的过程为导向的编程示例。

什么是进程为导向的编程?

让我们从定义开始: 以过程为导向的编程 是一个基于的范例 传达顺序过程,原本来自纸张 Tony Haare. 1977年。这也很受欢迎地称为 演员 并发模型。与此原始工作有关的其他语言包括COMPAM,LIMBO和GO。正式纸张仅处理同步沟通;大多数演员模型(包括 OTP.)也使用异步通信。始终可以在异步通信之上构建同步通信,并且OTP支持两种形式。

在此历史记录中,OTP通过通信顺序过程创建了一个用于容错计算的系统。容错设施来自“让IT失败”方法,以主管的形式具有实体错误恢复以及演员模型使能的分布式处理的使用。 “让它失败”可以与“防止失败失败”对比,因为前者更容易容纳,并且已经在OTP中被证明比后者更可靠。原因是防止失败所需的编程工作(如Java检查的异常模型所示)更有涉及和苛刻。

因此,面向过程的编程可以定义为一个 范例,其中系统的过程之间的过程结构和通信是主要问题.

面向对象的与进程为导向的编程

在面向对象的编程中,数据和功能的静态结构是主要问题。操作封闭数据需要哪些方法,以及对象或类之间的连接应该是什么。因此,UML的类图是该焦点的主要示例,如图1所示。

以过程为导向的编程:示例UML类图

可以注意到,对面向对象的编程的共同批判是没有可见的控制流程。因为系统由单独定义的大量类/对象组成,所以对于更少经验的人来说,可能难以可视化系统的控制流程。对于具有大量继承的系统尤其如此,它使用抽象接口或没有强大的打字。在大多数情况下,它对 开发人员 要记住大量的系统结构,以有效(哪个类具有哪些方法并且以什么方式使用)。

面向对象的开发方法的强度是,可以扩展系统以支持对现有代码影响有限的新类型对象,只要新的对象类型符合现有代码的期望即可。

功能与进程为导向的编程

许多功能编程语言以各种方式进行地址并发性,但它们的主要焦点是在函数之间传递的不可变数据,或者从其他功能创建功能(生成函数的高阶函数)。在大多数情况下,语言的焦点仍然是单个地址空间或可执行文件,并且这种可执​​行文件之间的通信以特定于操作系统的方式处理。

例如, Scala是一种功能语言 构建在Java虚拟机上。虽然它可以访问Java设施进行通信,但它不是语言的固有部分。虽然它是在Spark编程中使用的常规语言,但它再次与语言结合使用的库。

功能范式的强度是可视化给定顶级功能的系统的控制流程的能力。控制流程是显式的,因为每个函数调用其他函数,并将所有数据从一个传递到下一个。在功能范式中,没有副作用,这使得问题确定更容易。纯功能系统的挑战是“副作用”需要具有持久状态。在井架系统中,持续存在于控制流程的顶级,允许大部分系统自由副作用。

酏剂 / OTP和以过程为导向的编程

在Elixir / Erlang和OTP中,通信基元是执行语言的虚拟机的一部分。在进程和机器之间进行通信的能力是内置于语言系统的中核。这强调了在此范例和这些语言系统中沟通的重要性。

虽然Elixir语言主要是 功能 就语言表达的逻辑而言,它的使用是 进程导向.

以过程为导向是什么意思?

如本篇文章所定义的进程导向是首先以过程存在的形式设计系统以及它们如何进行通信。主要问题之一是哪个进程是静态的,它是动态的,这是按需向请求产生的动态,这提供了一种长期运行的目的,其保存系统的共享状态或部分共享状态,以及哪些功能系统本质上并发。正如OO具有对象类型,并且功能具有功能类型,以过程为导向的编程都具有过程类型。

因此, 面向过程的设计是识别解决问题或满足需求所需的一组过程类型.

时间的方面进入了设计和需求的努力。系统的生命周期是什么?偶尔的自定义需求是多少?系统中的负载在哪里以及预期的速度和卷是什么?只有在这些类型的考虑之后,应该理解的是,面向过程的设计开始定义每个过程的功能或要执行的逻辑。

培训含义

该分类对培训的含义是,培训不应该以语言语法或“Hello World”的例子开始,而是 系统工程思维与设计专注于过程分配.

编码问题是次要的过程设计和分配,该分配是在更高级别的最佳地址,并涉及关于生命周期,QA,Devops和客户业务需求的跨函数思考。 Elixir或Erlang中的任何培训课程都必须(通常是)包括OTP,并且应该从一开始就具有流程方向,而不是“现在您可以在Elixir中代码,因此让我们做并发”类型方法。

采用影响

采用的含义是,语言和系统更好地应用于需要通信和/或分布计算的问题。单个计算机上单个工作负载的问题在此空间不太有趣,并且可以更好地与另一种语言寻址。长期的连续处理系统是此语言的主要目标,因为它具有从下面内置的容错容差。

对于文档和设计工作,使用图形表示法非常有帮助(如图1用于OO语言)。从UML进行Elixir和进程导向的编程的建议将是序列图(图2中的示例),以显示进程之间的时间关系,并标识哪个进程涉及服务请求。没有用于捕获生命周期和过程结构的UML图表类型,但它可以用一个简单的框和流程类型及其关系的箭头图来表示。例如,图3:

面向过程编程样本UML序列图

面向过程编程样品工艺结构图

过程方向的一个例子

最后,我们将走过一个将流程方向应用于问题的简短示例。假设我们是任务提供支持全球选举的系统。选择此问题的选项中的许多单独的活动在突发中进行,但是实时期望的结果或结果的总结,并且可能会看到显着的负载。

初始流程设计和分配

我们最初可以看到每个人的投票铸造是从许多离散输入到系统的流量突发,不是时间有序,并且可以具有高负载。为了支持此活动,我们希望大量的进程全部收集这些输入并将它们转发到更高的列表进程。这些过程可以位于将产生投票的每个国家的群体附近,从而提供低延迟。它们会保留本地结果,立即记录其输入,然后转发它们以批量标记为减少带宽和开销。

我们最初可以看到需要在必须追踪每个司法管辖区中的投票的流程。让我们假设我们需要跟踪每个国家/地区的结果,并通过省/州地区的每个国家。为了支持此活动,我们希望每个国家执行计算,并保留当前总计,以及每个国家/地区的每个州/省的另一个进程。这假设我们需要能够实时或低延迟回答国家和州/省的总数。如果可以从数据库系统获取结果,我们可能会选择不同的过程分配,其中总数由瞬态进程更新。使用这些计算的专用过程的优点是结果以存储速度发生,并且可以以低延迟获得。

最后,我们可以看到很多人和很多人都会看到结果。这些过程可以以多种方式进行分区。我们可能希望通过在负责该国家的结果的每个国家的流程放置流程来分发负载。该过程可以缓存来自计算过程的结果,以减少计算过程上的查询加载,并且/或计算过程可以在结果地将它们的结果推向适当的结果过程,当结果通过大量变化或on计算过程变得闲置,表明更大的变化率。

在所有三种流程类型中,我们可以彼此独立扩展进程,地理位置地分发它们,并确保通过在进程之间的数据传输的主动确认实现结果永远不会丢失。

如上所述,我们已经开始使用流程设计,独立于每个进程中的业务逻辑。在业务逻辑对数据聚合或地理的特定要求的情况下,可以迭代地影响过程分配。我们的工艺设计到目前为止如图4所示。

以过程为导向的发展示例:初始流程设计

使用单独的流程接收投票允许每次投票独立于任何其他投票,并在收到后登录,并批量到下一组流程,显着降低了这些系统的负载。对于消耗大量数据的系统,通过使用流程层减少数据量是常见和有用的模式。

通过在隔离的一组过程中执行计算,我们可以管理这些过程的负载并确保其稳定性和资源要求。

通过将结果呈现放在一个隔离的过程中,我们将负载控制到系统的其余部分,并允许一组流程动态地缩放为负载。

其他要求

现在,让我们添加一些复杂的要求。让我们假设在每个管辖区(国家或国家)中,投票的列表可能导致比例结果,获胜者所带来的所有结果,如果投票不足相对于该管辖权的人口,则不会产生结果。每个司法管辖区都控制了这些方面。有了这种变化,那么国家的结果并不是对原始投票结果的简单汇总,而是州/省的汇总。这将改变原始过程分配,要求州/省进程的结果进入国家流程。如果投票集合与州/省和省与国家进程之间使用的协议是相同的,则可以重复使用聚合逻辑,但需要持有结果的不同过程,并且它们的通信路径不同,如图所示5。

以过程为导向的发展示例:修改过程设计

编码

要完成示例,我们将审核Elixir OTP中的示例的实现。为了简化事物,此示例假定像Phoenix这样的Web服务器用于处理实际的Web请求,并且这些Web服务使请求对上面识别的进程作出请求。这具有简化示例并将重点放在Elixir / OTP上的优点。在生产系统中,具有单独的过程具有一些优点以及分离问题,允许灵活的部署,分配负载并减少延迟。可以找到具有测试的完整源代码 //github.com/technomage/voting。源在此帖子中缩写以进行可读性。下面的每个过程都适用于OTP监控树,以确保在故障时重新启动进程。在示例的这个方面,请参阅源。

投票记录员

此过程接收投票,将它们记录到持久存储器,并将结果批量批量到聚合器。模块Voterecoder使用Task.SuperVisor来管理短暂的任务以记录每种投票。

defmodule Voting.VoteRecorder do
  @moduledoc """
  This module receives votes and sends them to the proper
  aggregator. This module uses supervised tasks to ensure
  that any failure is recovered from and the vote is not
  lost.
  """

  @doc """
  Start a task to track the submittal of a vote to an
  aggregator. This is a supervised task to ensure
  completion.
  """
  def cast_vote where, who do
    Task.Supervisor.async_nolink(Voting.VoteTaskSupervisor,
      fn ->
        Voting.Aggregator.submit_vote where, who
      end)
    |> Task.await
  end
end

投票聚合器

此过程在管辖区内聚合投票,计算该管辖区的结果,并将投票摘要转发给下一个更高的过程(更高级别的管辖区或结果演示者)。

defmodule Voting.Aggregator do
  use GenStage
  ...

  @doc """
  Submit a single vote to an aggregator
  """
  def submit_vote id, candidate do
    pid = __MODULE__.via_tuple(id)
    :ok = GenStage.call pid, {:submit_vote, candidate}
  end

  @doc """
  Respond to requests
  """
  def handle_call {:submit_vote, candidate}, _from, state do
    n = state.votes[candidate] || 0
    state = %{state | votes: Map.put(state.votes, candidate, n+1)}
    {:reply, :ok, [%{state.id => state.votes}], state}
  end

  @doc """
  Handle events from subordinate aggregators
  """
  def handle_events events, _from, state do
    votes = Enum.reduce events, state.votes, fn e, votes ->
      Enum.reduce e, votes, fn {k,v}, votes ->
        Map.put(votes, k, v) # replace any entries for subordinates
      end
    end
    # Any jurisdiction specific policy would go here

    # Sum the votes by candidate for the published event
    merged = Enum.reduce votes, %{}, fn {j, jv}, votes ->
      # Each jourisdiction is summed for each candidate
      Enum.reduce jv, votes, fn {candidate, tot}, votes ->
        Logger.debug "@@@@ Votes in #{inspect j} for #{inspect candidate}: #{inspect tot}"
        n = votes[candidate] || 0
        Map.put(votes, candidate, n + tot)
      end
    end
    # Return the published event and the state which retains
    # Votes by jourisdiction
    {:noreply, [%{state.id => merged}], %{state | votes: votes}}
  end
end

结果主持人

此过程从聚合器接收投票,并将这些结果缓存为用于呈现结果的服务请求。

defmodule Voting.ResultPresenter do
  use GenStage
  …

  @doc """
  Handle requests for results
  """
  def handle_call :get_votes, _from, state do
    {:reply, {:ok, state.votes}, [], state}
  end

  @doc """
  Obtain the results from this presenter
  """
  def get_votes id do
    pid = Voting.ResultPresenter.via_tuple(id)
    {:ok, votes} = GenStage.call pid, :get_votes
    votes
  end

  @doc """
  Receive votes from aggregator
  """
  def handle_events events, _from, state do
    Logger.debug "@@@@ Presenter received: #{inspect events}"
    votes = Enum.reduce events, state.votes, fn v, votes ->
      Enum.reduce v, votes, fn {k,v}, votes ->
        Map.put(votes, k, v)
      end
    end
    {:noreply, [], %{state | votes: votes}}
  end
end

带走

这篇文章将Elixir / OTP从其潜力作为面向过程的语言探索,这与面向对象和功能的范例相比,并审查了这一点以培训和采用的影响。

该帖子还包括将此方向应用于样本问题的简短示例。如果您想查看所有代码,这是我们示例的链接 GitHub. 再次,就这样你就不必滚动回来寻找它。

关键的外卖是将系统视为传送流程的集合。从一个过程设计的观点来计划系统,以及逻辑编码的逻辑编码点。

理解基础知识

什么是elixir和OTP?

酏剂是一种在Erlang VM上构建的功能编程语言。 OTP是一个以Erlang和Elixir为导向的编程框架。

什么是过程导向的发展?

以过程为导向的开发专注于系统的过程结构,以及系统的功能逻辑。

最好采用Elixir / OTP和进程为导向的发展所需的内容?

从培训或探索开始,专注于OTP和流程管理,然后在Elixir的语法和功能方面。避免培训从Hello World编码示例开始,只能通过OTP半路。

为什么采用Elixir / OTP和以过程为导向的发展?

酏剂 / OTP的可靠性和并发方面是高于竞争堆栈的头部和肩部,需要更少的程序员技能才能熟练,并且在Rails或Node上的Ruby具有更好的性能。

什么时候选择Elixir / OTP?

酏剂 / OTP用于长期运行进程或需要多核性能的过程。它们比高吞吐量更多地关注低延迟。对于要求苛刻的单一核心吞吐量应用,或者在批处理或命令行环境中不经常运行的应用程序并不强大。