Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala pattern matching in a concurrent program

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

like image 998
Oren Avatar asked Aug 14 '12 03:08

Oren


2 Answers

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.

like image 141
Malte Schwerhoff Avatar answered Nov 15 '22 06:11

Malte Schwerhoff


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.

like image 39
Kim Stebel Avatar answered Nov 15 '22 05:11

Kim Stebel