Unintended change to local variable of a Scala Act

2019-08-19 07:53发布

问题:

I am currently working on a logging mechanism to be used with Scala, but I have run into an unexpected issue that has prevented me from actually working on it. In order to test functionality, I am looking to set-up a simple message passing ring. Within the ring, each node is an extension of a Scala Actor and is aware of it's immediate neighbors (previous/next).

The ring construction is done as follows, with the parameter "nodes" being passed from a controller actor:

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)
  }
}

This appears to function as I would like it to, with each node receiving the correct pair of neighbors. In the node class there is an array of size 2 which is set as follows:

class node(locLogger:logger,nid:Int,boss:Actor) extends Actor{
  val ringNeighbors:Array[Actor] = new Array[Actor](2)
  def act{
    locLogger.start
    loop{
        receive{
            case n:Array[Actor] =>
              ringNeighbors(0) = n(0)
              ringNeighbors(1) = n(1)

Everything is fine through is point, however, when I introduce a message to be passed around the ring (from node 0), I have discovered that every node now has the same values in it's ringNeighbors array. These values are consistent with the final iteration of the loop in the ringBuilder (i.e. node 8's neighbors) function. There is no additional message passing which occurs so I do not understand how these values were modified for each instance in the nodes array and thus the ring.

I am still learning the ropes of Scala and hope I did not overlook something simple by mistake.

回答1:

I think the problem is the following:

The Actor model is essentially an asynchronous model, meaning that actors process messages at times indifferent to the sending times.

You are sending to each of your actor a reference to a size 2 array, which keeps changing its content depending on the state of your iteration. However, actors will not process the initialization message right after the call nodes(i) ! spliceArr. So what probably happens is that the iteration finishes and only after that the actors are scheduled to process the messages. The trouble is, all of them see the instance of spliceArr as it is when the for loop has finished.

So the simple solution is to not send an array but a pair:

nodes(i) ! spliceArr

becomes

nodes(i) ! (nodes(i-1), nodes(i+1))

and you should also modify the corresponding lines before the loop. This change should be performed also in the code of the actors - use tuples instead of arrays for this kind of stuff.

If my guess was right, then the core problem is that you're using mutable data structures (the array in your case) which are shared amongst various entities (the actors, in your example). This always leads to problems so unless the application you are working on really has a particular need for stateful data structures you should always bet on immutability.

Now, in the particular case of actor systems the messages exchanged between actors have an even greater need to be immutable. Actors are supposed to be enclosed data structures and their state should not be accessible from the exterior. Also, in an actor system there should be no global state.

Unfortunately, unlike other languages that implement actor systems such as Erlang, Scala cannot enforce this behavior. Therefore it is the job of the developer to ensure this happens.

Mutable messages are bad since they can cause actors to share state - the state contained in the message which in the context of the actor's concurrent execution will probably lead to hard to spot issues.

Here's how the code would look like with the fixes described above:

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)
                }
            }
        }
    }
}

I have also made some changes for greater readability of the algorithm, I hope you don't mind.