对Scala Actor的局部变量的意外更改

对Scala Actor的局部变量的意外更改,scala,actor-model,Scala,Actor Model,我目前正在开发一个与Scala一起使用的日志机制,但我遇到了一个意外的问题,使我无法实际使用它。为了测试功能,我希望设置一个简单的消息传递环。在环中,每个节点都是Scala Actor的扩展,并且知道它的近邻(上一个/下一个) 环构造如下所示,参数“nodes”由控制器参与者传递: def buildRing(nodes:Array[Actor]){ var spliceArr:Array[Actor] = new Array[Actor](2) spliceArr(0) = nodes

我目前正在开发一个与Scala一起使用的日志机制,但我遇到了一个意外的问题,使我无法实际使用它。为了测试功能,我希望设置一个简单的消息传递环。在环中,每个节点都是Scala Actor的扩展,并且知道它的近邻(上一个/下一个)

环构造如下所示,参数“nodes”由控制器参与者传递:

def buildRing(nodes:Array[Actor]){
  var spliceArr:Array[Actor] = new Array[Actor](2)
  spliceArr(0) = nodes(nodes.length-1)
  spliceArr(1) = nodes(1)
  nodes(0) !  spliceArr
  Thread.sleep(100)
  spliceArr(0) = nodes(nodes.length-2)
  spliceArr(1) = nodes(0)
  nodes(nodes.length-1) ! spliceArr
  Thread.sleep(100)
  for(i <-1 to numNodes-2){
      spliceArr(0) = nodes(i-1)
      spliceArr(1) = nodes(i+1)
      nodes(i) ! spliceArr
      Thread.sleep(100)
  }
}
通过这一点,一切都很好,但是,当我引入一条要在环中传递的消息(来自节点0)时,我发现现在每个节点在其RingNeights数组中都有相同的值。这些值与ringBuilder(即节点8的邻居)函数中循环的最终迭代一致。没有发生额外的消息传递,因此我不理解如何为节点数组中的每个实例修改这些值,从而修改环


我仍在学习Scala的诀窍,希望我没有错误地忽略一些简单的东西

我认为问题在于:

参与者模型本质上是一个异步模型,这意味着参与者处理消息的时间与发送时间无关

您正在向每个参与者发送对大小为2的数组的引用,该数组根据迭代的状态不断更改其内容。但是,参与者不会在调用
节点(i)之后立即处理初始化消息!拼接ARR
。因此,可能发生的情况是迭代完成,并且只有在参与者被安排处理消息之后。问题是,它们都看到了for循环完成时的
spaiterar
实例

因此,简单的解决方案是不发送数组,而是发送一对:

nodes(i) ! spliceArr
变成

nodes(i) ! (nodes(i-1), nodes(i+1))
您还应该在循环之前修改相应的行。这种改变也应该在参与者的代码中执行——对于这种东西,使用元组而不是数组

如果我的猜测是正确的,那么核心问题是您正在使用可变数据结构(在您的示例中是数组),这些数据结构在不同实体(在您的示例中是参与者)之间共享。这总是会导致问题,因此除非您正在处理的应用程序确实需要有状态的数据结构,否则您应该始终押注于不变性

现在,在参与者系统的特殊情况下,参与者之间交换的消息更需要是不可变的。参与者应该是封闭的数据结构,其状态不应从外部访问。此外,在参与者系统中,不应该有全局状态

不幸的是,与其他实现角色系统(如Erlang)的语言不同,Scala无法强制执行此行为。因此,开发人员的工作就是确保实现这一点

可变消息是不好的,因为它们会导致参与者共享状态——消息中包含的状态在参与者并发执行的上下文中可能会导致难以发现的问题

下面是使用上述修复程序时代码的外观:

def buildRing(nodes: Array[Actor]) {
    nodes.zipWithIndex.foreach {
        case (actor, index) => actor ! (previous(nodes, index), next(nodes, index))
    }
}

//Gets the next actor from the ring for the specified index.
def next(nodes: Array[Actor], index: Int): Actor = {
    val k = (index + 1) % nodes.length
    nodes(k)
}

//Gets the previous actor
def previous(nodes: Array[Actor], index: Int): Actor = {
    val k = if (index == 0) nodes.length - 1 else index - 1
    nodes(k)
}

class Node(locLogger:logger, nid:Int, boss:Actor) extends Actor {
    private var leftNeighbour: Option[Actor] = None //avoid using null in favor of Option
    private var rightNeighbour: Option[Actor] = None
    def act {
        locLogger.start
        loop {
            receive {
                case (left, right) => {
                    leftNeighbour = Some(left)
                    rightNeighbour = Some(right)
                }
            }
        }
    }
}

为了提高算法的可读性,我也做了一些修改,希望您不要介意。

我认为问题如下:

参与者模型本质上是一个异步模型,这意味着参与者处理消息的时间与发送时间无关

您正在向每个参与者发送对大小为2的数组的引用,该数组根据迭代的状态不断更改其内容。但是,参与者不会在调用
节点(i)之后立即处理初始化消息!拼接ARR
。因此,可能发生的情况是迭代完成,并且只有在参与者被安排处理消息之后。问题是,它们都看到了for循环完成时的
spaiterar
实例

因此,简单的解决方案是不发送数组,而是发送一对:

nodes(i) ! spliceArr
变成

nodes(i) ! (nodes(i-1), nodes(i+1))
您还应该在循环之前修改相应的行。这种改变也应该在参与者的代码中执行——对于这种东西,使用元组而不是数组

如果我的猜测是正确的,那么核心问题是您正在使用可变数据结构(在您的示例中是数组),这些数据结构在不同实体(在您的示例中是参与者)之间共享。这总是会导致问题,因此除非您正在处理的应用程序确实需要有状态的数据结构,否则您应该始终押注于不变性

现在,在参与者系统的特殊情况下,参与者之间交换的消息更需要是不可变的。参与者应该是封闭的数据结构,其状态不应从外部访问。此外,在参与者系统中,不应该有全局状态

不幸的是,与其他实现角色系统(如Erlang)的语言不同,Scala无法强制执行此行为。因此,开发人员的工作就是确保实现这一点

可变消息是不好的,因为它们会导致参与者共享状态——消息中包含的状态在参与者并发执行的上下文中可能会导致难以发现的问题

下面是使用上述修复程序时代码的外观:

def buildRing(nodes: Array[Actor]) {
    nodes.zipWithIndex.foreach {
        case (actor, index) => actor ! (previous(nodes, index), next(nodes, index))
    }
}

//Gets the next actor from the ring for the specified index.
def next(nodes: Array[Actor], index: Int): Actor = {
    val k = (index + 1) % nodes.length
    nodes(k)
}

//Gets the previous actor
def previous(nodes: Array[Actor], index: Int): Actor = {
    val k = if (index == 0) nodes.length - 1 else index - 1
    nodes(k)
}

class Node(locLogger:logger, nid:Int, boss:Actor) extends Actor {
    private var leftNeighbour: Option[Actor] = None //avoid using null in favor of Option
    private var rightNeighbour: Option[Actor] = None
    def act {
        locLogger.start
        loop {
            receive {
                case (left, right) => {
                    leftNeighbour = Some(left)
                    rightNeighbour = Some(right)
                }
            }
        }
    }
}

为了提高算法的可读性,我也做了一些修改,希望您不介意。

谢谢您的详细解释。我将使用这种方法,看看它是否解决了不一致性问题。当然,这似乎是一个并发问题,它通常是很难解决的,但是你的解释确实给了我以前没有的方向。请考虑所有的东西,除了<代码> Actudio>代码>私下。