Helsingin yliopisto / tietojenkäsittelytieteen laitos / Ohjelmointitekniikka (Scala) / © Arto Wikla 2012

30 Aktoreista ja rinnakkaisuudesta lyhyesti

Muutettu viimeksi 15.2.2012 / Sivu luotu 23.4.2010 / [oppikirjan esimerkit] / [Scala]

Sivun sisältöä:

Tässä luvussa nähdään vain Scalan aktoreiden perusidea. Rinnakkaisuuden syvällisempi käsittely säästetään muille kursseille.

Perusidea

Scalassa on toki käytettävissä Javan koko rinnakkaisohjelmointikalusto, mutta sen lisäksi kielen kirjastossa on välineet ns. aktorimallin käyttämiseen rinnakkaisuuden toteuttamisessa. Kirjasto osaltaan myös havainnollistaa Scalan voimaa omien kontrollirakenteiden toteuttamisessa.

Yksi rinnakkaisohjelmoinnin monista ongelmista on jaetun muistin suojaus. Aktorimallissa jaettu muisti ei tuota ongelmia, koska aktorit eivät jaa muistia! Aktorit sen sijaan lähettelevät toisilleen viestejä ja vastaanottavat muilta aktoreilta tulleita viestejä. Viestien vastaanottoon aktorilla on postilaatikko.

Aktoreita voi luoda scala.actors.Actor-luokan aliluokkina (tai tässä "aliolioina") ohjelmoimalla act()-metodin:

  import scala.actors._

  object SillyActor extends Actor {
    def act() {
      for (i <- 1 to 5) {
        println("I'm acting!")
        Thread.sleep(700)
      }
    }
  }
  object SeriousActor extends Actor {
    def act() { 
      for (i <- 1 to 5) {
        println("To be or not to be.")
        Thread.sleep(1000)
      }
    }
  }

  SillyActor.start
  SeriousActor.start

Aktori käynnistetään start-metodilla. Ohjelma tulostaa hidastellen esim.:
  To be or not to be.
  I'm acting!
  I'm acting!
  To be or not to be.
  I'm acting!
  To be or not to be.
  I'm acting!
  I'm acting!
  To be or not to be.
  To be or not to be.

Aktorin voi valmistaa myös metodilla scala.actors.Actor.actor:
  import scala.actors.Actor._   // (huom: importoidaan luokan metodit!)

  val hamlet = actor {
    for (i <- 1 to 5) {
      println("That is the question.")
      Thread.sleep(1000)
    }
  }   
Näin luotu aktori käynnistyy saman tien ilman mitään start-pyyntöä.

Viestit

Viestejä lähtetään syntaksilla
   aktori ! viesti
Kun aktori lähettää viestin, se ei pysähdy odottamaan mitään. Kun aktori saa viestin, sen toiminta ei keskeydy, mutta halutessaan aktori voi mennä lukemaan postilaatikkoaan lauseella receive:
  receive {
    case jotakin: Tyyppi => toimintaa
    case jotakinmuuta: ToinenTyyppi => muuta toimintaa
    ...
    case eityyppiä => toimintaa muiden kuin nimettyjen tyyppien tapauksessa
  }

Jos postilaatikosta ei löydy mitään vastaanotettavan tyyppistä viestiä, aktori odottaa, kunnes sellainen tulee. Jos viimeisenä on tyypitön case-vaihtoehto, minkä tahansa tyyppinen viesti vastaanotetaan.

Esimerkki.

import scala.actors.Actor._

object Kaiku extends App {

  val kaiuttaja = actor {
    while (true) {
      receive {  // tälle mikä vain kelpaa
        case msg =>
          println("vastaanotettu viesti: "+ msg)
      }

    }
  }

  val lukuTehdas = actor {
    for (i <- 1 to 10) {
      kaiuttaja ! i
      Thread.sleep(700)
    }
  }

  val kissaTehdas = actor {
    for (i <- 1 to 10) {
      kaiuttaja ! "kissa numero " + i
      Thread.sleep(1000)
    }
  }
}
Esimerkkitulostus (huom: ohjelman saa päättymään ctrl-c:llä):
vastaanotettu viesti: kissa numero 1
vastaanotettu viesti: 1
vastaanotettu viesti: 2
vastaanotettu viesti: kissa numero 2
vastaanotettu viesti: 3
vastaanotettu viesti: kissa numero 3
vastaanotettu viesti: 4
vastaanotettu viesti: 5
vastaanotettu viesti: kissa numero 4
vastaanotettu viesti: 6
vastaanotettu viesti: kissa numero 5
vastaanotettu viesti: 7
vastaanotettu viesti: 8
vastaanotettu viesti: kissa numero 6
vastaanotettu viesti: 9
vastaanotettu viesti: kissa numero 7
vastaanotettu viesti: 10
vastaanotettu viesti: kissa numero 8
vastaanotettu viesti: kissa numero 9
vastaanotettu viesti: kissa numero 10

Esimerkki, jossa eri tyyppisiä viestejä käsitellään eri tavoin:

import scala.actors.Actor._

object EriKaiku extends App {

  val kaiuttaja = actor {
    while (true) {
      receive {
        case msg: String =>
          println("vastaanotettu merkkijono:   "+ msg)
        case msg: Int =>

          println("vastaanotettu kokonaisluku: "+ msg)
        case msg =>
          println("vastaanotettu jotakin:      "+ msg)
      }
    }
  }

  val lukuTehdas = actor {
    for (i <- 1 to 4) {
      kaiuttaja ! i
      Thread.sleep(700)
    }
  }
  val kissaTehdas = actor {
    for (i <- 1 to 4) {
      kaiuttaja ! "kissa numero " + i

      Thread.sleep(1000)
    }
  }

  val jokinTehdas = actor {
    for (i <- 1 to 3) {
      kaiuttaja ! List(3,1)
      Thread.sleep(500)
      kaiuttaja ! Map("koira" -> "hau", "kissa" -> "miau")
    }
  }
}
Esimerkkitulostus (huom: ohjelman saa päättymään ctrl-c:llä):
vastaanotettu merkkijono:   kissa numero 1
vastaanotettu kokonaisluku: 1
vastaanotettu jotakin:      List(3, 1)
vastaanotettu jotakin:      Map(koira -> hau, kissa -> miau)
vastaanotettu jotakin:      List(3, 1)
vastaanotettu kokonaisluku: 2
vastaanotettu merkkijono:   kissa numero 2
vastaanotettu jotakin:      Map(koira -> hau, kissa -> miau)
vastaanotettu jotakin:      List(3, 1)
vastaanotettu kokonaisluku: 3
vastaanotettu jotakin:      Map(koira -> hau, kissa -> miau)
vastaanotettu merkkijono:   kissa numero 3
vastaanotettu kokonaisluku: 4
vastaanotettu merkkijono:   kissa numero 4


Tuotantoketju

Yksi paljon käytetty ohjelmiston arkkitehtuurin malli on tuottaja-kuluttajamalli. Aktoreilla sellaisen ohjelmointi on luontevaa:
import scala.actors.Actor._

object Tuotantoketju extends App {

  val alkutuotanto = actor {
    while (true) {                     
      Thread.sleep(500)  // Töitä tehdään...
      println("Koneet käy: runks, runks, runks, ...")
      jalostus ! math.random
    }
  }
  val jalostus = actor {
    while (true) {
      receive {
        case msg: Double =>
           println("Jalostetaan " + msg)
           koristelu ! (msg * 1000 toInt)
      }
    }
  }
  val koristelu = actor {
   while (true) {
      receive {
        case msg =>
          println("Koristellaan " + msg)
          myynti ! "** " + msg + " **"
      }
    }
  }
  val myynti = actor {
    var lkm = 0
    while (true) {
      receive {
        case msg =>
          lkm += 1
          println("Myyty tuote " + msg + ". Kokonaismyynti: " + lkm + " kpl.")
      }
    }
  }
}

Esimerkkitulostusta (huom: ohjelman saa päättymään ctrl-c:llä):
Koneet käy: runks, runks, runks, ...
Jalostetaan 0.09351960144560689
Koristellaan 93
Myyty tuote ** 93 **. Kokonaismyynti: 1 kpl.
Koneet käy: runks, runks, runks, ...
Jalostetaan 0.8703921180993694
Koristellaan 870
Myyty tuote ** 870 **. Kokonaismyynti: 2 kpl.
Koneet käy: runks, runks, runks, ...
Jalostetaan 0.6863182839097028
Koristellaan 686
Myyty tuote ** 686 **. Kokonaismyynti: 3 kpl.
Koneet käy: runks, runks, runks, ...
Jalostetaan 0.9782239841378854
Koristellaan 978
Myyty tuote ** 978 **. Kokonaismyynti: 4 kpl.
Koneet käy: runks, runks, runks, ...
Jalostetaan 0.7204195334294463
Koristellaan 720
Myyty tuote ** 720 **. Kokonaismyynti: 5 kpl.
Koneet käy: runks, runks, runks, ...
Jalostetaan 0.9264337052100224
Koristellaan 926
Myyty tuote ** 926 **. Kokonaismyynti: 6 kpl.
Koneet käy: runks, runks, runks, ...
Jalostetaan 0.6971132918108124
Koristellaan 697
Myyty tuote ** 697 **. Kokonaismyynti: 7 kpl.
Koneet käy: runks, runks, runks, ...
...