• Scala学习二十——Actor


    一.本章要点

    • 每个actor都要扩展Actor类并提供act方法
    • 要往actor发送消息,可以用actor!message
    • 消息发送是异步的:”发完就忘“
    • 要接受消息,actor可以调用receive或react,通常是在循环中这样做
    • receive/react的参数是有case语句组成的代码块(偏函数)
    • 不同actor之间不应该共享状态。总是使用消息来发送数据
    • 不要直接调用actor的方法,通过消息进行通信
    • 避免同步消息——将发送消息和等待响应分开
    • 不同actor可以通过react而不是receive来共享线程,前提是消息处理器的控制流转足够简单
    • 让actor挂掉是OK的,前提是你有其他actor监控着actor的生死。用链接来设置监控关系

    二.创建和启动Actor

      actor是扩展自Antor特质的类。该特质有一个抽象方法act。可以重写这个方法指定avtor的行为。act方法带有一个消息循环,例:

    import scala.actors.Actor
    
    class HiActor extends Actor{
    def act(){while(true){
    receive{
    case "Hi"=>println("Hello")}
    }}
    }
    //调用start方法执行
    val actor1=new HiActor
    actor1.start()

      act方法与Java中Runable接口中的run方法很相似,正如不同线程的run方法那样,不同actor的act方法也是并行运行的,对响应信息做了优化。  

      临时创建actor,利用Actor伴生对象的actor方法创建和启动actor:

    import scala.actors.Actor._
    val actor=actor{
    while(true){
    receive{case "Hi"=>println("Hello")
    }
    }
    }

    三.发送消息

      actor是一个处理异步消息的对象。

      消息可以是任何对象。

      使用!操作符发送小心,消息被发送当前线程继续执行——”发完就忘“(可以等待一个回复),一个好的做法是使用样例类作为消息,让actor使用模式匹配来处理消息。

    四.接受消息

      发送到actor的消息被存放在一个”邮箱“中,receive方法从邮箱获取下一条信息并将它传递给它的参数,该参数是一个偏函数。例:

    receive {
    case Deposit(amount)=>....
    case Withdraw(amount)=>...
    }
    //receive的参数是{case Deposit(amount)=>....
    //case Withdraw(amount)=>...
    //}

      该代码块被转换成一个类型为PartialFunction[Any,T]的对象,其中T是case语句=>操作符右边的表达式的计算结果的类型。这是个偏函数,因为它对那些能够匹配其中一个case语句的参数有定义;

      注:消息传递的过程是异步的。(到达顺序不确定,设计时应让应用程序不要依赖任何特定的消息投递顺序);

        receive方法被调用时并没有消息,则该调用阻塞,直到有消息抵达,如果邮箱中没有任何消息可以被偏函数处理,则对receive方法的调用也会阻塞,直到一个可以匹配的消息抵达;  

        邮箱有可能被那些不与任何case语句匹配的消息占满。添加一个case _语句来处理任意的消息;

        邮箱会串行化信息:actor运行在单个线程中(不用担心争用状况);

        actor可以很安全的修改自己的数据(但如果修改了在不同actor之间共享的数据,那么争用情况就有可能出现),因此不要在不同的actor中使用共享对象(除非是线程安全的)    

    五.向其他Actor发送消息

      当运算被拆分到不同的actor来并行处理问题的各个部分时,这些处理结果需要被收集到一起。actor可以将结果存入到一个线程安全的数据结构当中(如并发的哈希映射),但actor模型并不鼓励使用共享数据。因此当actor计算结果后,应该向另一个actor发送消息。

      actor是如何知道应该往哪里发送计算结果:

        1.可以有一些全局的actor(当actor数量很多时,这个方案的伸缩性不好);

        2.actor可以构造成带有指向一个或多个actor的引用(当antor持有另一个actor的引用时,它只应该使用这个引用来发送消息,而不是调用方法。这样做违背了actor精神,同时还可能引发争用状况——这正是actor设计出来要避免的问题);

        3.actor可以接收带有指向另一个actor的引用消息。在请求中提供一个actor引用是很常见的做法,例:actor ! Compute(date,continuation)

        4.actor可以返回消息给发送方,receive方法会把sender字段设为当前消息的发送方

    六.消息通道

      除了在应用程序中对actor共享引用的做法,还可以共享消息通道给它们。

      优点:1.消息通道是类型安全的——你只能发送或接受某个特定类型的消息;2.不会不小心通过消息通道调用到某个actor的方法。

      消息通道可以是一个OutputChannel(带有!方法),也可以是一个InputChannel(带有receive或react方法)。Channel类同时扩展OutputChannel和InputChannel特质。

      构造一个消息通道(需要提供一个actor):val channel=new Channel[Int][someActor]

      如果不提供构造参数,那么消息通道就会绑定到当前执行这个actor上:

    case class Compute(input:Seq[Int],result:OutputChannel[Int])
    class Computer extends Actor{
    public void act(){
    while(true){
    receive{
    case Compute(input,output)=>{val anwser=...;out!anwser}
    }}
    }
    }
    
    actor {
    val channel=new Channel[Int]
    val computeActor:Computer=...
    val input:Seq[Double]=...
    computeActor!Compute(input,channel)
    channel.receive{
    case x=>...//已知x是一个Int
    }
    }

      注意:这里调用的是channeldereceive而不是actor自己,如果想要通过actor来接收相应,可以匹配一个!样例类的实例

    七.同步消息和Future

      actor可以发送一个消息并等待回复,用!?操作符即可,例:

    val reply=account!?Deposit(1000)
    reply match{
    case Balance(bal)=>println("Current Balance:"+bal)
    }

      要让它工作,接收方必须返回一个消息给发送方:  

    receive {
    case Deposit(amount)=>{
    //除了用sender!Balance(balance),也可以写reply(Balance(balance))
    balance+=amount;sender!Balance(balance)
    ...
    }
    }

      注:同步消息容易引发死锁,通常而言,最好避免在actor的act方法里执行阻塞调用。

      使用receiveWithin方法指定需要等待多少毫秒的时间(不想对一个回复永久等待),如果在指定时间没有收到消息,就会收到一个Actor.TIMEOUT对象,例:

    actor {
    worker !Task(data,self)
    receiveWithin(secends*1000){
    case Result(data)=>,,,
    case TIMEOUT=>lof(...)
    }
    }

      注:react方法也有一个带时间的版本,称做reactWithin。

      除了等待对方返回结果之外,也可以选择接收一个future——这是一个将在结果可用时产出结果的对象,使用!!方法得到:val replyFuture=ammount!!Deposit(1000),isSet方法会检查结果是否可用,要接受结果,使用函数调用的表示法:val replyFuture=replyFuture(),这个调用将会阻塞,直到消息被发送。注:如果立即想从future接受返回的结果,那么你并没有享受到任何优于同步方法调用的好处,不过,actor可以将future放到一边,稍后再做处理,或将它交给其他actor。

    八.共享线程

       上述在Scala中,react方法接受一个偏函数,并将它添加到邮箱,然后退出,例:

    react {//偏函数f1
    case Withdraw(amount)=>{
    react {//偏函数f2
    case Confirm()=>{
    println("Confirming"+amount)
    }
    }
    }
    }
    //第一个react的调用将f1与actor的邮箱关联起来,然后退出。当Withdraw消息抵达时,f1被调用,偏函数f1也调用react,这次调用把f2与actor的邮箱关联起来,然后退出,当Confirm消息抵达时,f2被调用

      注:第二个react可能在一个单独的函数中,因此,react需要抛出异常,从而得以退出。

        关联的偏函数不会返回一个值——它执行了某些工作,然后执行下一个react,就造成退出了。退出意味着返回到协调的actor的方法中,这样的一个函数的返回类型为Nothing,该类型用来表示正常退出。

    //由于react会退出,因此不能简单的把它放在while循环中,如:
    
    def act(){
    while(true){
    react{//偏函数
    case Withdraw(amount)=>println("Withdrawing"+amount)
    }
    }
    }
    //当act被调用时,对react的调用将f1与邮箱关联起来,然后退出,当f1被调用时,将会处理这条消息
    {case Withdraw(amount)=>println("Withawing "+amount)}
    //解决方法一:在消息处理器中再次调用act方法(无穷递归替换掉无穷循环,这个无穷递归不会占用很大的栈空间,每次对react的调用都会抛出异常从而清栈)
    def act(){
    react{//偏函数
    case Wirhdraw(amount)=>{
    println("Withawing"+amount)
    act()
    }
    }
    }
    
    //让每个消息处理器自己负责保持循环继续进行下去看上去并不很公平,于是有一些”控制流转组合子“可以自动产出这些循环
    //loop组合子可以制作一个无穷循环
    def act(){
    loop{
    react{
    case Withdraw(amount)=>process(amount)}
    }}
    //如果需要一个循环条件,可以用loopWhile
    loopWhile(count>max){
    react{
    ...}
    }
    
    //eventloop方法可以制作一个无穷循环套react的简化版,不过提前是偏函数不会再次调用react
    def act(){
    eventloop{
    case Withdraw(amount)=>println("Withdrawing"+amount)
    }
    }

    九.Actor的生命周期

      actor的act方法在actor的start方法被调用时开始执行。actor接下来做的事情是进入某个循环,例:

    def act(){
    while(...){
    receive{
    ...}
    }
    }
    //actor在如下情形之一会终止执行:

      1.act方法返回
      2.act方法由于异常被终止
      3.actor调用exit方法
    ×/ 

      注意:exit方法是个受保护的方法。它只能被Actor的子类中调用(其他方法不能调用exit方法来终止一个actor)。例:

    //其他方法也不能调用exit()来终止
    val actor1=actor {
    while(true){
    receive{
    case "Hi"=>println("Hello")
    case "Bye"=>exit()
    }
    }
    }

      还有一个exit方法的重载版本可以接受一个参数描述退出原因。不带参数调用exit相当于exit('normal)。

      当actor因一个异常终止时,退出原因就是UncaughtException样例类的一个实例,该样例类有如下属性:

        • actor:抛出异常的actor
        • message:Some(msg),其中msg是该actor处理的最后一条消息或者None,如果actor在没来得及处理任何消息之前就挂掉的话;
        • sender:Some(channel),其中channel是代表最后一条消息的发送方的输出消息通道或者None,如果actor在没来得及处理任何消息之前就挂掉的话;
        • thread:actor退出时所在的线程;
        • case:相应的异常

    十.将多个Actor链接在一起

      将两个actor链接在一起,则另一个都会在另一个终止执行的时候得到通知(调用link方法即可):

    def act(){
    link(master)
    }

      链表是双向的(如监管actor在数个工作actor中分发工作任务,那么当某个工作actor挂掉的时候,监管actor应该知道,以便相应的工作任务可以重新指派,反过来,如果监控actor挂掉了,工作actor也应该知道,以便可以停止工作)

      注:尽管链接是双向的,但link方法并不是对称的,不能呢将link(worker)替换成worker.link(self),该方法必须由请求链接的actor调用。

      默认情况下,只要当前actor链接到的actor中有一个非’normal原因退出,当前actor就会终止(在这种情况下,退出原因和链接到那个actor的退出原因相同)。

      actor可以改变这个行为,做法是设置trapExit为true,修改后mactor会接受到一个类型为Exit的消息,该消息包含了那个正在终止的actor和退出的原因。例:

    override def act(){
    trapExit=true
    link(wprker)
    while(...){
    receive{
    ...
    case Exit(linked,UncaughtException(_,_,_,case))=>...
    case Exit(linked,reason)=>...
    }
    }
    }

      在操作actor时,允许它们挂掉是正常的。只需要把每个actor链接到一个”监管“actor,由它来处理失败的actor,例把工作重新分配或者重新启动。

      

      

    十一.Actor的设计

      使用和操作actor注意:

    十二.练习

  • 相关阅读:
    第二章:变量和简单数据类型
    第四章:操作列表
    第三章:列表简介
    老男孩Day6作业:计算器
    老男孩Day5作业:电子银行购物商城
    老男孩Day4作业:员工信息查询系统
    老男孩Day3作业:工资管理系统
    老男孩Day2作业:购物车程序
    改进地图的vo类
    slam kf
  • 原文地址:https://www.cnblogs.com/lyq-biu/p/11984038.html
Copyright © 2020-2023  润新知