Pregunta Coincidencia de patrones Scala en un programa concurrente


Soy nuevo en Scala, y quiero escribir algunos códigos de subprocesos múltiples con coincidencia de patrones, y me preguntaba si podría tratar el código de coincidencia de patrones como atómico.

Por ejemplo:

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

¿Es posible que la ejecución sea la siguiente?

  1. El hilo principal alcanza el código de "coincidir con punto global", descubre que "punto_global" no es de tipo TwoDim y pausas (vuelve al programador).
  2. El hilo generado cambia * global_point * para ser de tipo TwoDim
  3. El hilo principal vuelve, descubre que el * global_point * no es de tipo OneDim, piensa que no hay coincidencias con * global_point * y genera una excepción NoMatch.

¿Este tipo de ejecución es evitada internamente por Scala? Si lo hace, entonces ¿cómo? ¿La coincidencia toma una instantánea del objeto y luego trata de compararlo con los patrones? ¿Hay un límite para la profundidad de la instantánea (los patrones de coincidencia pueden ser complejos y anidados)?


5
2017-08-14 03:17


origen


Respuestas:


Esto no es evidencia sólida de las especificaciones, pero ilustra algunas cosas que el compilador hace por ti, y eso debería permitirte considerar unos pocos como bloques atómicos pero definitivamente no todos. Será mucho más seguro si sincroniza su código usted mismo o si va con objetos inmutables.

Ejemplo plano

Si ejecuta el siguiente script con scala -print:

var m: Option[String] = _

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

Verá el código intermedio desugared creado por el compilador (he eliminado algo de código por brevedad):

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

El posible objeto compartido referenciado por m obtiene un alias local temp1, asi que si se cambia m en el fondo, de modo que apunta a otro objeto, entonces la coincidencia todavía tiene lugar en el objeto antiguo m señaló a Por lo tanto, la situación que describió anteriormente (cambiando global_point apuntar a un TwoDim en lugar de un OneDim) no será un problema.

Ejemplo anidado

En general, parece ser que el compilador crea alias locales para todos los objetos que están enlazados en la protección de un caso de coincidencia, ¡pero no crea una copia profunda! Para el siguiente 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"
}

El compilador crea este código intermedio:

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

Aquí, el compilador crea alias locales para el objeto. x se empareja, y por sus dos sub-objetos x.f (obligado a f) y x.x (obligado a ix), pero no para ix.f. Por lo tanto, si la estructura con la que se empareja está profundamente anidada y sus casos dependen de objetos anidados que no se unen localmente, entonces pueden ocurrir condiciones de carrera. Y lo haremos, como todos sabemos gracias a Murphy.


4
2017-08-14 07:18



No creo que haya ningún tipo de bloqueo o captura de instantáneas en el código generado para la coincidencia de patrones. Si lo fuera, seguramente sería mencionado en la especificación del lenguaje. Dicho esto, si coloca el código de coincidencia de patrones en un método, al menos puede estar seguro de que la referencia que se pasa al método no cambiará mientras se ejecuta el método. Si TwoDim y OneDim también son inmutables, entonces no debe preocuparse por la seguridad de los hilos. Incluso si no son inmutables, no importa siempre que no coincidan en uno de sus campos mutables.


1
2017-08-14 07:01



En Scala se usa la solución preferida para la concurrencia. Actors. En esos Actors, necesitas usar la coincidencia de patrones para agregar comportamiento. Construí un ejemplo usando scalas Actor similar al tuyo.

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
}

Primero creé los mensajes. Solo envias inmutables entre Actors, por lo que utilicé objetos de casos. También podría ajustar las respuestas, pero en este caso no es necesario. Luego creé dos tipos de esclavos, ellos solo devolvieron un nuevo MyPoint si reciben un. Y finalmente creé un maestro. El maestro inicializa dos esclavos, uno de cada tipo, y los inicia. Después envía 10 veces. Next a cada. Luego recibe las respuestas y las imprime. Cuando se reciben todas las respuestas, el maestro envía End a cada esclavo y termina. Obtuve el siguiente resultado:

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

1
2017-08-14 07:13