scala akka_使用Scala开发现代应用程序:Akka的并发性和并行性

scala akka

本文是我们名为“ 使用Scala开发现代应用程序 ”的学院课程的一部分。

在本课程中,我们提供一个框架和工具集,以便您可以开发现代的Scala应用程序。 我们涵盖了广泛的主题,从SBT构建和响应式应用程序到测试和数据库访问。 通过我们简单易懂的教程,您将能够在最短的时间内启动并运行自己的项目。 在这里查看

可以公平地说,如今,具有单CPU(或仅一个内核)的计算机时代已成为被人们遗忘的历史。 现在,大多数设备,无论它们有多小,都是使用旨在提高整体计算能力的多核CPU架构构建的。

硬件方面的这些进步和创新迫使我们重新思考如何开发和运行软件系统以有效利用所有可用资源的方法。

1.简介

在本教程的这一部分中,我们将讨论现代软件系统所依赖的两个核心概念:并发性和并行性。 尽管它们彼此之间非常接近,但还是有一些微小但重要的区别。 并发描述了多个任务可以随时间推移进行的过程,而并行性描述了同时执行多个任务时的过程。

2.线程

传统上,大多数并发编程模型都是围绕线程构建的,Java也不例外。 一个典型的JVM应用程序(作为一个进程运行)可能会产生许多线程,以便同时执行,或者最好并行执行一些工作。

jvm过程

图1典型的JVM进程产生了几个线程

这种模型在一段时间内运行良好,但是基于线程的并发模型至少存在一个基本缺陷:状态或资源共享。 在大多数情况下,线程需要交换共享一些数据或利用另一个共享资源来完成其工作。 如果没有适当的机制,对共享资源的不受控制的访问或多个线程对共享状态的修改(称为竞争条件 )会导致数据损坏,并经常导致应用程序崩溃。

同步化

图2使用同步访问共享状态

正确使用同步原语(锁,互斥体,信号量,监视器等)可以解决访问共享状态(或资源)的问题,但是付出的代价确实很高。 它不仅使编程模型复杂化,而且多线程执行流的不确定性使问题排查过程非常耗时且困难。 而且,出现了一类新的问题:锁争用,线程饥饿,死锁,活动锁等等。

随之而来的是,线程和线程管理会消耗大量底层操作系统资源,并且从本质上讲,在某个时候创建​​更多线程实际上会对应用程序性能和可伸缩性产生负面影响。

3.React堆和事件循环

基于线程的并发模型的缺陷使业界的注意力转向寻找更能充分满足现代软件系统需求的替代方案。 React堆模式事件处理是现代异步和无阻塞编程范例的基础,是一些需要并发大规模调用的新兴编程模型。

React堆

图3React器模式(简化)

请注意,这是对Reactor模式的一种可能实现的非常简化的可视化,但是它很好地说明了其关键要素。 Reactor的核心是单线程事件循环。 实际上,事件处理可以使用一个或多个线程(通常分组在一个池中)来进行实际工作,但是此模型中线程的用途和用法有很大不同,并且与应用程序完全隔离。

4.演员和消息

消息传递 ,或者更准确地说,异步消息传递是另一个非常有趣且功能强大的并发模型,该模型最近获得了很大的关注。 Actor Model起源于1973年,是异步消息传递并发的最好例子之一。

演员是演员模型的核心实体。 每个参与者都有自己的消息队列(称为邮箱),单线程消息处理程序,并且仅通过异步消息与其他参与者进行通信(不忽略参与者也可以创建另一个参与者的事实)。

演员

图4阿卡族演员

这是典型的无共享架构:参与者可能具有自己的状态,但是他们从未与任何其他参与者共享任何东西。 Actor可以生活在相同的JVM进程中,或者在同一物理节点上的多个JVM进程中,甚至可以散布在网络上,只要它们能够相互引用并通过消息进行通信就没有关系。

5.认识阿卡

当我们谈论React性应用程序并了解Akka Streams时,我们已经遇到了Akka工具包 。 但是,回顾一下历史,值得一提的是, Akka工具包已经开始作为JVM平台上的Actor Model实现。 从那时起,它已经看到了许多发行版(最近的发行版是2.4.10 ),获得了许多附加的特性和功能,但是即使在今天,演员仍然是Akka的骨干。

ActorSystemAkka演员宇宙的切入点。 它是应用程序中创建和管理参与者的唯一位置。

implicit val system = ActorSystem("akka-actors")

这样,我们就可以创建新演员了! 在Akka中 ,每个actor都应该继承Actor特质(或与Actor特质混合)并实现receive功能,例如:

import akka.actor.Actor

class SampleActor extends Actor {
  def receive = {
    ...
  }
}

但是比平时更常见的是,您还可以将ActorLogging特性包含在组合中,以使用日志参考来访问专用的记录器,例如:

import akka.actor.Actor
import akka.actor.ActorLogging

class SampleActor extends Actor with ActorLogging {
  def receive = {
    case _ => log.info("Received some message!")
  }
}

尽管目前我们的SampleActor并没有做很多事情,但我们可以实例化它并向其发送任何消息。 如前所述,仅通过ActorSystem实例而不是使用new运算符来创建actor ,例如:

val sampleActor = system.actorOf(Props[SampleActor], "sample-actor")
sampleActor ! "Message!"

如果您认为sampleActor变量是SampleActor类的实例,则不太正确。 它实际上是对SampleActor actor的引用,表示为ActorRef类。 这是解决Akka中特定actor的唯一机制,无法直接访问基础actor类实例。

当我们运行应用程序时会发生什么? 除了我们应该在控制台中看到类似内容的事实外,其他内容不多:

[INFO] [akka-actors-akka.actor.default-dispatcher-2] [akka://akka-actors/user/sample-actor] Received some message!

6.监督

Akka中,演员有趣但非常重要的特性是他们是按等级组织的。 自然地,演员可以产生子演员以将作品分成较小的部分,从而形成父子层次。

这听起来像是次要的细节,但这并不是因为在Akka中,父母演员可能会看着孩子,这个过程称为监督 。 在这种情况下,父级参与者本质上会成为主管,并且在子级参与者遇到失败的情况下(或者更具体地讲,会因异常终止)可以采用不同的策略。

官方文档详细讨论了违约和不同的监管策略,但让我们看一个快速的示例。 我们的ChildActor定义为始终在收到任何Message时引发异常。

class ChildActor extends Actor with ActorLogging {
  def receive = {
    case Message(m) => 
      throw new IllegalStateException("Something unexpected happened")
  }
}

因此,我们的ParentActor actor创建ChildActor actor的实例,并将收到的任何消息转发给ChildActor实例。

class ParentActor extends Actor with ActorLogging {
  val child = context.actorOf(Props[ChildActor])
  
  override val supervisorStrategy = OneForOneStrategy() {
    case _: IllegalStateException => Resume
    case _: Exception => Escalate
  }

  def receive = {
    case _ => child ! Message("Message from parent")
  }
}

根据默认的监视策略,引发Exception的actor将被重新启动(在大多数情况下,这可能是所需的行为)。 但是,在我们的示例中,仅在IllegalStateException情况下,我们重写了此策略(使用ParentActor supervisorStrategy属性),以恢复对监督参与者( ChildActor )的正常消息处理。

7.模式

在其基本形式中, Akka中的参与者通过异步的单向消息进行通信。 它当然可以工作,但是许多现实世界中的场景都需要更复杂的交互,例如使用请求/回复通信,断路器,参与者与其他参与者之间的管道消息。 幸运的是, akka.pattern程序包提供了一组易于使用的常用Akka模式。 例如,让我们稍微更改SampleActor实现,以处理Message类型的Message并在收到Message后使用MessageReply回复。

case class Message(message: String)
case class MessageReply(reply: String)

class SampleActor extends Actor with ActorLogging {
  def receive = {
    case Message(m) => sender ! MessageReply(s"Reply: $m")
  }
}

现在,我们可以采用Asking模式,以便将消息发送给actor并等待答复,例如:

import akka.pattern.ask
import akka.util.Timeout

implicit val timeout: Timeout = 1 second
val reply = (sampleActor ? Message("Please reply!")).mapTo[MessageReply]

在这种情况下,发送方将消息发送给参与者,并等待回复(超时时间为1秒)。 请注意,典型的Akka actor不支持有关消息的任何类型安全语义:任何东西都可以发送和接收作为响应。 发送方有责任进行适当的类型转换(例如,使用mapTo方法)。 同样,如果发件人将消息发送给不知道如何处理的角色,则消息将以空字母结尾。

8.类型演员

如前所述, Akka的演员在接受或回复消息时不提供任何类型的安全性。 但是在相当长的一段时间内, Akka都提供了对Typed Actors的实验支持,其中的联系是显式的,并且将由编译器强制执行。

Typed Actor的定义与常规的Akka actor完全不同,并且非常类似于我们用来构建RPC样式系统的方式 。 首先,我们必须首先定义接口及其实现,例如:

trait Typed {
  def send(message: String): Future[String]
}

class SampleTypedActor extends Typed {
  def send(message: String): Future[String] = Future.successful("Reply: " + message)
}

反过来, 类型Actor的实例化方式需要更多代码,尽管仍在幕后使用ActorSystem

implicit val system = ActorSystem("typed-akka-actors")
  
val sampleTypedActor: Typed = 
  TypedActor(system).typedActorOf(TypedProps[SampleTypedActor]())
  
val reply = sampleTypedActor
  .send("Hello Typed Actor!")
  .andThen { case Success(r) => println(r) }

此时,您可能会想到一个合乎逻辑的问题:不是应该在所有地方都使用Typed Actor ? 好点,但简短的答案是:不,可能不会。 如果您感到好奇,请花一些时间来讨论有关使用Typed Actor与常规,非Typed Actor的利弊的精彩讨论

9.调度程序

除了提供Actor Model的高级实现之外, Akka还提供了许多非常有用的实用程序。 其中之一是调度支持 ,它提供了定期或在某个时间点向特定参与者发送消息的功能。

implicit val system = ActorSystem("akka-utilities")
import system.dispatcher
  
val sampleActor = system.actorOf(Props[SampleActor], "sample-actor")  
system.scheduler.schedule(0 seconds, 100 milliseconds, sampleActor, "Wake up!")

毋庸置疑,大多数实际应用程序都要求能够安排一些任务的执行时间,因此开箱即用此功能非常方便。

10.事件总线

Akka的另一个非常有用的实用程序包含通用事件总线概念以及由ActorSystem事件流的形式提供的特定实现。

如果演员之间的通信假定消息的发送者通过事件流以某种方式知道接收者是谁,则演员可以选择向任何其他演员广播任何事件(某种类型的消息),而无需事先知道谁会收到它。 在这种情况下,有关当事方必须通过订阅此类事件来表达其兴趣。

例如,假设我们有一条重要消息,我们将其简单地命名为Event

case class Event(id: Int)

我们指定SampleEventActor处理这种消息,并在每次收到消息时在控制台上打印出该消息的id

class SampleEventActor extends Actor with ActorLogging {
  def receive = {
    case Event(id) => log.info(s"Event with '$id' received")
  }
}

看起来很简单,但目前还没有什么令人兴奋的。 现在,让我们看一下事件流,并发布/订阅实际的通信模式。

implicit val system = ActorSystem("akka-utilities")
  
val sampleEventActor = system.actorOf(Props[SampleEventActor])  
system.eventStream.subscribe(sampleEventActor, classOf[Event])
  
system.eventStream.publish(Event(1))
system.eventStream.publish(Event(2))
system.eventStream.publish(Event(3))

我们的sampleEventActor通过调用system.eventStream.subscribe()方法表示有兴趣接收Event类型的消息。 现在,每次要通过system.eventStream.publish()调用发布Event时,无论发布者是谁, sampleEventActor都将接收它。 启用日志记录后,我们将在控制台输出中看到类似的内容:

[INFO] [akka-utilities-akka.actor.default-dispatcher-2] [akka://akka-utilities/user/$a] Event with '1' received
[INFO] [akka-utilities-akka.actor.default-dispatcher-2] [akka://akka-utilities/user/$a] Event with '2' received
[INFO] [akka-utilities-akka.actor.default-dispatcher-2] [akka://akka-utilities/user/$a] Event with '3' received

11.远程处理

到目前为止,我们看到的所有示例都只涉及一个ActorSystem ,它在一个节点内的单个JVM中运行。 但是Akka的网络扩展支持多JVM /多节点部署,因此不同的ActorSystem可以在真正的分布式环境中相互通信。

为了启用ActorSystem远程功能,我们需要更改其默认的Actor参考提供程序并启用网络传输。 使用application.conf配置文件可以轻松完成所有操作:

akka {
  actor {
    provider = "akka.remote.RemoteActorRefProvider"
  }
  
  remote {
    enabled-transports = ["akka.remote.netty.tcp"]
    netty {
	    tcp {
	      hostname = "localhost"
	      port = ${port}
	    }
	}
  }
}

作为练习,我们将运行两个ActorSystem实例,一个在端口12000上名为akka-remote-1 ,另一个在端口12001上运行akka-remote-2 。 我们还将定义一个与之通信的Actor SampleRemoteActor

class SampleRemoteActor extends Actor with ActorLogging {
  def receive = {
    case m: Any => log.info(s"Received: $m")
  }
}

在第一个ActorSystem上akka-remote-1 ,我们将创建SampleRemoteActor的实例并向其发送一条消息。

implicit val system = ActorSystem("akka-remote-1")
  
val sampleActor = system.actorOf(Props[SampleRemoteActor], "sample-actor")
sampleActor ! "Message from Actor System #1!"

但是在第二个akka-remote-2 ,我们将使用其远程引用将消息发送到SampleRemoteActor实例,其中包括ActorSystem名称( akka-remote-1 ),主机( localhost ),端口( 12000 ),并指定演员名称(在本例中为sample-actor ):

implicit val system = ActorSystem("akka-remote-2")
  
val sampleActor = system.actorSelection(
  "akka.tcp://akka-remote-1@localhost:12000/user/sample-actor")
sampleActor ! "Message from Actor System #2!"

很简单,不是吗? 并排运行两个ActorSystem实例将在akka-remote-1 JVM进程的控制台中产生以下输出:

[INFO] [main] [akka.remote.Remoting] Starting remoting
[INFO] [main] [akka.remote.Remoting] Remoting started; listening on addresses :[akka.tcp://akka-remote-1@localhost:12000]
[INFO] [main] [akka.remote.Remoting] Remoting now listens on addresses: [akka.tcp://akka-remote-1@localhost:12000]
[INFO] [akka-remote-1-akka.actor.default-dispatcher-2] [akka.tcp://akka-remote-1@localhost:12000/user/sample-actor] Received: Message from Actor System #1!
[INFO] [akka-remote-1-akka.actor.default-dispatcher-4] [akka.tcp://akka-remote-1@localhost:12000/user/sample-actor] Received: Message from Actor System #2!

到目前为止,除了我们已经看到的内容外,不仅一个actor系统可以引用另一个actor系统中的actor ,它还可以远程创建新的actor实例。

12.测试

Akka提供了出色的测试支持,以确保可以涵盖演员行为和互动的每个方面。 实际上, Akka TestKit为编写传统的单元测试和集成测试提供了适当的框架

单元测试技术围绕TestActorRef进行 ,这是对常规ActorRef实现的简化,不涉及并发性,并且无法访问actor状态内部。 让我们从这一点开始,并使用我们已经熟悉的specs2框架为SampleActor进行简单的单元测试。

class SampleActorTest extends Specification with AfterAll {
  implicit val timeout: Timeout = 1 second
  implicit lazy val system = ActorSystem("test")
  
  "Sample actor" >> {
    "should reply on message" >> { implicit ee: ExecutionEnv =>
      val actorRef = TestActorRef(new SampleActor)
      actorRef ? Message("Hello") must be_==(MessageReply("Reply: Hello")).await
    }
  }
  
  def afterAll() = {
    system.terminate()
  }
}

单元测试当然是一个很好的开始,但与此同时,由于它依赖于系统的简化视图,因此通常非常受限制。 但是,再次感谢Akka TestKit ,我们可以使用更强大的测试技术。

class SampleActorIntegrationTest extends TestKit(ActorSystem("test")) 
    with ImplicitSender with WordSpecLike with BeforeAndAfterAll {
  
  "Sample actor" should {
    "should reply on message" in {
      val actorRef = system.actorOf(Props[SampleActor])
      actorRef ! Message("Hello")
      expectMsg(MessageReply("Reply: Hello"))
    }
    
    "should log an event" in {
      val actorRef = system.actorOf(Props[SampleActor])      
      EventFilter.info(
        message = "Event with '100' received", occurrences = 1) intercept {
        actorRef ! Event(100)
      }
    }
  }
  
  override def afterAll() = {
    shutdown()
  }
}

这次,我们使用了ScalaTest框架的观点,并采用了稍微不同的方法来依赖TestKit类,该类提供了一组关于消息期望的丰富断言 。 我们不仅要刺探消息的能力,我们也能够让使用过预期的日志记录断言EventFilter类,通过支持TestEventListenerapplication.conf文件。

akka {
  loggers = [
    akka.testkit.TestEventListener
  ]
}

真的很好,测试用例看起来简单,可读且可维护。 但是, Akka测试功能并不止于此,而且还在不断发展。 例如,值得一提的是实验性多节点测试支持的可用性。

13.结论

Akka是一个了不起的工具包,并为许多其他库和框架奠定了坚实的基础。 作为Actor Model的实现,它还提供了另一种使用异步消息传递并促进不变性的并发和并行性方法。

值得一提的是,这些天Akka一直在积极开发,并且已经超越了Actor模型 。 随着每个新版本的发布,它都包含越来越多的工具(稳定或/和实验性的),用于构建高度并发和分布式的系统。 它的许多高级功能(例如集群持久性有限状态机路由等)都没有涉及,但是Akka官方文档是熟悉所有这些功能的最佳场所。

14.接下来是什么

在本教程下一部分中,我们将讨论Play! 框架 :强大,高效且功能丰富的框架,用于在Scala中构建可扩展的成熟Web应用程序。

翻译自: https://www.javacodegeeks.com/2016/10/developing-modern-applications-scala-concurrency-parallelism-akka.html

scala akka

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值