5
votes

I'm new to Scala, and I want to write some multi-threaded code with pattern matching, and I was wondering if I could treat the pattern-matching code as atomic.

For example:

abstract class MyPoint
case class OneDim(x : Int) extends MyPoint
case class TwoDim(x : Int, y : Int) extends MyPoint

var global_point : MyPoint = new OneDim(7)

spawn {
    Thread.sleep(scala.util.Random.nextInt(100))
    global_point = new TwoDim(3, 9)
}
Thread.sleep(scala.util.Random.nextInt(100))

match global_point {
    case TwoDim(_, _) => println("Two Dim")
    case OneDim(_) => println("One Dim")
}

Is it possible that the execution will go as the following:

  1. The main thread reaches the "match global_point" code, finds out that *global_point* is not of type TwoDim and pauses (returns to the scheduler).
  2. The spawned thread changes *global_point* to be of type TwoDim
  3. The main thread returns, finds out that the *global_point* is not of type OneDim, thinks there are no matches to *global_point* and raises a NoMatch exception.

Is this kind of execution avoided internally by Scala? If it does, then how? Does the matching take a snapshot of the object and then try to match it against the patterns? Is there a limit to the snapshot depth (the match patterns can be complex and nested)?

3

3 Answers

4
votes

This is not hard evidence from the specs, but it illustrates some things the compiler does for you, and that should allow you to regard a few match blocks as atomic - but definitely not all. It will be much safer if you synchronise your code yourself, or if you go with immutable objects.

Flat example

If you run the following script with scala -print:

var m: Option[String] = _

m match {
  case Some(s) => "Some: " + s
  case None => "None"
}

you'll see the desugared intermediate code created by the compiler (I've removed some code for brevity):

final class Main$$anon$1 extends java.lang.Object {
  private[this] var m: Option = _;

  private <accessor> def m(): Option = Main$$anon$1.this.m;

  def this(): anonymous class Main$$anon$1 = {
    <synthetic> val temp1: Option = Main$$anon$1.this.m();

    if (temp1.$isInstanceOf[Some]()) {
      "Some: ".+(temp1.$asInstanceOf[Some]().x())
    else if (scala.this.None.==(temp1))
      "None"
    else
      throw new MatchError(temp1)
  }
}

The possibly shared object referenced by m gets a local alias temp1, so if m is changed in the background such that it points to another object, then the match still takes place on the old object m pointed to. Hence, the situation you described above (changing global_point to point to a TwoDim instead of to a OneDim) is not a problem.

Nested example

It seems to generally be the case that the compiler creates local aliases to all objects that are bound in the guard of a match case, but it does not create a deep copy! For the following script:

case class X(var f: Int, var x: X)

var x = new X(-1, new X(1, null))

x match {
  case X(f, ix) if f >  0 || ix.f > 0  => "gt0"
  case X(f, ix) if f <= 0 || ix.f <= 0 => "lte0"
}

the compiler creates this intermediate code:

private[this] var x: anonymous class Main$$anon$1$X = _;

private <accessor> def x(): anonymous class Main$$anon$1$X = Main$$anon$1.this.x;

final <synthetic> private[this] def gd2$1(x$1: Int, x$2: anonymous class Main$$anon$1$X): Boolean = x$1.>(0).||(x$2.f().>(0));

final <synthetic> private[this] def gd3$1(x$1: Int, x$2: anonymous class Main$$anon$1$X): Boolean = x$1.<=(0).||(x$2.f().<=(0));

def this(): anonymous class Main$$anon$1 = {
  <synthetic> val temp6: anonymous class Main$$anon$1$X = Main$$anon$1.this.x();

  if (temp6.ne(null)) {
    <synthetic> val temp7: Int = temp6.f();
    <synthetic> val temp8: anonymous class Main$$anon$1$X = temp6.x();
    
    if (Main$$anon$1.this.gd2$1(temp7, temp8))
      "gt0"
    else if (Main$$anon$1.this.gd3$1(temp7, temp8))
      "lte0"
    else
      throw new MatchError(temp6)
  } else
    throw new MatchError(temp6)
}

Here, the compiler creates local aliases for the object x you match on, and for its two sub-objects x.f (bound to f) and x.x (bound to ix), but not for ix.f. Hence, if the structure you match on is deeply nested and your cases depend on nested objects that you don't bind locally, then race conditions can occur. And will, as we all know, due to Murphy's Law.

1
votes

I do not think there is any kind of locking or snapshotting going on in the code generated for pattern matching. If it was, it would surely be mentioned in the language specification. That being said, if you put the pattern matching code into a method, you can at least be sure that the reference that method gets passed won't change while the method executes. If TwoDim and OneDim are also immutable, then you don't need to worry about thread safety. Even if they aren't immutable, it doesn't matter as long as you don't match on one of their mutable fields.

1
votes

In Scala the prefered solution for concurency is using Actors. In those Actors, you need to use pattern-matching to add behavior. I built an example using scalas Actor similar to yours.

import scala.actors.Actor
import scala.actors.Actor._

abstract class MyPoint
case class OneDim(x: Int) extends MyPoint
case class TwoDim(x: Int, y: Int) extends MyPoint

case object Next
case object End

class OneDimSlave extends Actor {

  def act() {
    println("Starting OneDimSlave")
  loop {
        receive {
          case End => println("Stoping OneDimSlave"); exit()
          case Next => sender ! OneDim(scala.util.Random.nextInt(100))
        }
      }
}

}

class TwoDimSlave extends Actor {

  def act() {
    println("Starting TwoDimSlave")
  loop {
    receive {
      case End => println("Stopping TwoDimSlave"); exit()
      case Next => sender ! TwoDim(scala.util.Random.nextInt(100),scala.util.Random.nextInt(100))
    }
  }
}

}

class Master extends Actor {

  val oneDimSlave = new OneDimSlave
  val twoDimSlave = new TwoDimSlave

  oneDimSlave.start
  twoDimSlave.start

  def act {
    println("Starting Master")
for (_ <- 0 to 9) { oneDimSlave ! Next; twoDimSlave ! Next }
var count = 0
loop {
  if (count >= 20) { oneDimSlave ! End; twoDimSlave ! End; println("Stopping Master"); exit() }
  receive {
    case _ :OneDim => count += 1; println(count + ". One Dim")
    case _: TwoDim => count += 1; println(count + ". Two Dim")
  }
}
  }

}

object Main extends App {
  val master = new Master
  master.start
}

First I created the messages. You only send immutables between Actors, so I used case objects. You could wrap the answers as well, but in this case there is no need for it. Next I created two kinds of slaves, they just return a new MyPoint if they receive a . And finally I created a Master. The master initializes two slaves, one of each kind, and starts them. Afterwards he sends 10 times Next to each. Then he receives the responses and prints them. When all response are received, the master send End to every slave and terminates. I got the following output:

Starting TwoDimSlave
Starting OneDimSlave
Starting Master
1. One Dim
2. Two Dim
3. Two Dim
4. Two Dim
5. Two Dim
6. Two Dim
7. Two Dim
8. Two Dim
9. Two Dim
10. Two Dim
11. Two Dim
12. One Dim
13. One Dim
14. One Dim
15. One Dim
16. One Dim
17. One Dim
18. One Dim
19. One Dim
20. One Dim
Stopping Master
Stopping OneDimSlave
Stopping TwoDimSlave