Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • lara/cs206-demos
  • gcharles/cs206-demos
  • gambhir/cs206-demos
3 results
Show changes
Showing
with 1022 additions and 4 deletions
/* Copyright 2009-2015 EPFL, Lausanne */
package concpar22final02.instrumentation
import java.lang.management.*
/** A collection of methods that can be used to collect run-time statistics */
object Stats:
def timed[T](code: => T)(cont: Long => Unit): T =
var t1 = System.currentTimeMillis()
val r = code
cont((System.currentTimeMillis() - t1))
r
def withTime[T](code: => T): (T, Long) =
var t1 = System.currentTimeMillis()
val r = code
(r, (System.currentTimeMillis() - t1))
package concpar22final03
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.Random
trait Economics:
/** A trading card from the game Scala: The Programming. We can own a card,
* but once don't anymore.
*/
final class Card(val name: String)
def isMine(c: Card): Boolean
/** This function uses the best available database to return the sell value of
* a card on the market.
*/
def valueOf(cardName: String): Int = List(1, cardName.length).max
/** This method represents an exact amount of money that can be hold, spent,
* or put in the bank
*/
final class MoneyBag()
def moneyIn(m: MoneyBag): Int
/** If you sell a card, at some point in the future you will get some money
* (in a bag).
*/
def sellCard(c: Card): Future[MoneyBag]
/** You can buy any "Scala: The Programming" card by providing a bag of money
* with the appropriate amount and waiting for the transaction to take place.
* You will own the returned card.
*/
def buyCard(money: MoneyBag, name: String): Future[Card]
/** This simple bank account holds money for you. You can bring a money bag to
* increase your account's balance, or withdraw a money bag of any size not
* greater than your account's balance.
*/
def balance: Int
def withdraw(amount: Int): Future[MoneyBag]
def deposit(bag: MoneyBag): Future[Unit]
class NotEnoughMoneyException
extends Exception("Not enough money provided to buy those cards")
package concpar22final03
import scala.concurrent.Future
import concurrent.ExecutionContext.Implicits.global
trait Problem3:
val economics: Economics
import economics.*
/** The objective is to propose a service of deck building. People come to you
* with some money and some cards they want to sell, and you need to return
* them a complete deck of the cards they want.
*/
def orderDeck(
bag: MoneyBag,
cardsToSell: List[Card],
wantedDeck: List[String]
): Future[List[Card]] =
Future {
val totalGivenMoney =
cardsToSell.foldLeft(moneyIn(bag))((sum, c) => sum + valueOf(c.name))
val totalNeededMoney =
wantedDeck.foldLeft(0)((sum, n) => sum + valueOf(n))
if totalGivenMoney < totalNeededMoney then
throw new NotEnoughMoneyException()
val soldCards: Future[Unit] =
if moneyIn(bag) != 0 then
sellListOfCards(cardsToSell).zip(deposit(bag)).map(_ => ())
else sellListOfCards(cardsToSell).map(_ => ())
soldCards.flatMap { _ => buyListOfCards(wantedDeck) }
}.flatten
/** This helper function will sell the provided list of cards and put the
* money on your personal bank account. It returns a Future of Unit, which
* indicates when all sales are completed.
*/
def sellListOfCards(cardsToSell: List[Card]): Future[Unit] =
val moneyFromSales: List[Future[Unit]] = cardsToSell.map { c =>
sellCard(c).flatMap(m => deposit(m).map { _ => })
}
Future
.sequence(moneyFromSales)
.map(_ =>
()
) // Future.sequence transforms a List[Future[A]] into a Future[List[A]]
/** This helper function, given a list of wanted card names and assuming there
* is enough money in the bank account, will buy (in the future) those cards,
* and return them.
*/
def buyListOfCards(wantedDeck: List[String]): Future[List[Card]] =
val boughtCards: List[Future[Card]] = wantedDeck.map { name =>
withdraw(valueOf(name)).flatMap(mb => buyCard(mb, name))
}
Future.sequence(boughtCards)
package concpar22final04
import akka.actor.*
import akka.testkit.*
import java.util.Date
import akka.event.LoggingReceive
import akka.pattern.*
import akka.util.Timeout
import concurrent.duration.*
import scala.concurrent.Future
import scala.concurrent.ExecutionContext
given timeout: Timeout = Timeout(200.millis)
/** Data associated with a song: a unique `id`, a `title` and an `artist`.
*/
case class Song(id: Int, title: String, artist: String)
/** An activity in a user's activity feed, representing that `userRef` is
* listening to `songId`.
*/
case class Activity(userId: String, userName: String, songId: Int)
/** Companion object of the `User` class.
*/
object User:
/** Messages that can be sent to User actors.
*/
enum Protocol:
/** Asks for a user name and id. Should be answered by a Response.Info.
*/
case GetInfo
/** Asks home page data. Should be answered by a Response.HomepageData.
*/
case GetHomepageData
/** Like song with id `songId`.
*/
case Like(songId: Int)
/** Unlike song with id `songId`.
*/
case Unlike(songId: Int)
/** Adds `subscriber` to the list of subscribers.
*/
case Subscribe(subscriber: ActorRef)
/** Remove `subscriber` from the list of subscribers.
*/
case Unsubscribe(subscriber: ActorRef)
/** Adds the activity `activity` to the activity feed. This message will be
* sent by the users this user has subscribed to.
*/
case AddActivity(activity: Activity)
/** Sent when a user starts playing a song with id `songId`. The recipient
* should notify all its subscribers to update their activity feeds by
* sending them `AddActivity(Activity(...))` messages. No answer is
* expected. This message is sent by external actors.
*/
case Play(songId: Int)
/** Asks for home page text. Should be answered by a Response.HomepageText.
*/
case GetHomepageText
/** Responses that can be sent back from User actors.
*/
enum Responses:
/** Answer to a Protocol.GetInfo message
*/
case Info(id: String, name: String)
/** Answer to a Protocol.GetHomepageData message
*/
case HomepageData(songIds: List[Int], activities: List[Activity])
/** Answer to a Protocol.GetHomepageText message
*/
case HomepageText(result: String)
/** The `User` actor, responsible to handle `User.Protocol` messages.
*/
class User(id: String, name: String, songsStore: ActorRef) extends Actor:
import User.*
import User.Protocol.*
import User.Responses.*
import SongsStore.Protocol.*
import SongsStore.Responses.*
given ExecutionContext = context.system.dispatcher
/** Liked songs, by reverse date of liking time (the last liked song must be
* the first must be the first element of the list). Elements of this list
* must be unique: a song can only be liked once. Liking a song twice should
* not change the order.
*/
var likedSongs: List[Int] = List()
/** Users who have subscribed to this users.
*/
var subscribers: Set[ActorRef] = Set()
/** Activity feed, by reverse date of activity time (the last added activity
* must be the first element of the list). Items in this list should be
* unique by `userRef`. If a new activity with a `userRef` already in the
* list is added, the former should be removed, so that we always see the
* latest activity for each user we have subscribed to.
*/
var activityFeed: List[Activity] = List()
/** This actor's behavior. */
override def receive: Receive = LoggingReceive {
case GetInfo =>
sender() ! Info(id, name)
case GetHomepageData =>
sender() ! HomepageData(likedSongs, activityFeed)
case Like(songId) if !likedSongs.contains(songId) =>
likedSongs = songId :: likedSongs
case Unlike(songId) =>
likedSongs = likedSongs.filter(_ != songId)
case Subscribe(ref: ActorRef) =>
subscribers = subscribers + ref
case Unsubscribe(ref: ActorRef) =>
subscribers = subscribers - ref
case AddActivity(activity: Activity) =>
activityFeed =
activity :: activityFeed.filter(_.userId != activity.userId)
case Play(songId) =>
subscribers.foreach(_ ! AddActivity(Activity(id, name, songId)))
case GetHomepageText =>
val likedSongsFuture: Future[Songs] =
(songsStore ? GetSongs(likedSongs)).mapTo[Songs]
val activitySongsFuture: Future[Songs] =
(songsStore ? GetSongs(activityFeed.map(_.songId))).mapTo[Songs]
val response: Future[HomepageText] =
for
likedSongs <- likedSongsFuture;
activitySongs <- activitySongsFuture
yield HomepageText(
f"""
|Howdy ${name}!
|
|Liked Songs:
|${likedSongs.songs
.map(song => f"* ${song.title} by ${song.artist}")
.mkString("\n")}
|
|Activity Feed:
|${activityFeed
.zip(activitySongs.songs)
.map((activity, song) =>
f"* ${activity.userName} is listening to ${song.title} by ${song.artist}"
)
.mkString("\n")}""".stripMargin.trim
)
response.pipeTo(sender())
}
/** Objects containing the messages a songs store should handle.
*/
object SongsStore:
/** Ask information about a list of songs by their ids.
*/
enum Protocol:
case GetSongs(songIds: List[Int])
/** List of `Song` corresponding to the list of IDs given to `GetSongs`.
*/
enum Responses:
case Songs(songs: List[Song])
/** A mock implementation of a songs store.
*/
class MockSongsStore extends Actor:
import SongsStore.Protocol.*
import SongsStore.Responses.*
import SongsStore.*
val songsDB = Map(
1 -> Song(1, "High Hopes", "Pink Floyd"),
2 -> Song(2, "Sunny", "Boney M."),
3 -> Song(3, "J'irai où tu iras", "Céline Dion & Jean-Jacques Goldman"),
4 -> Song(4, "Ce monde est cruel", "Vald"),
5 -> Song(5, "Strobe", "deadmau5"),
6 -> Song(6, "Désenchantée", "Mylène Farmer"),
7 -> Song(7, "Straight Edge", "Minor Threat"),
8 -> Song(8, "Hold the line", "TOTO"),
9 -> Song(9, "Anarchy in the UK", "Sex Pistols"),
10 -> Song(10, "Breakfast in America", "Supertramp")
)
override def receive: Receive = LoggingReceive { case GetSongs(songsIds) =>
sender() ! Songs(songsIds.map(songsDB))
}
/////////////////////////
// DEBUG //
/////////////////////////
/** Infrastructure to help debugging. In sbt use `run` to execute this code. The
* TestKit is an actor that can send messages and check the messages it
* receives (or not).
*/
@main def debug() = new TestKit(ActorSystem("DebugSystem")) with ImplicitSender:
import User.*
import User.Protocol.*
import User.Responses.*
import SongsStore.Protocol.*
import SongsStore.Responses.*
try
val songsStore = system.actorOf(Props(MockSongsStore()), "songsStore")
val anita = system.actorOf(Props(User("100", "Anita", songsStore)))
anita ! Like(6)
expectNoMessage() // expects no message is received
anita ! GetHomepageData
expectMsg(HomepageData(List(6), List()))
finally shutdown(system)
// This demonstrates how to import functions from a package inside a worksheet.
import midterm22.{task, find}
find(Array(1, 2, 3), 2, 1)
package ed
// To demonstrate different ways of pattern matching, let's consider the
// following example case class and instance:
case class Person(name: String, age: Int)
val ada = Person("Ada", 36)
// Run using `sbt "runMain ed.patternMatching"`.
@main def patternMatching =
// There are several ways to pattern match on the `ada` instance:
// 1. Pattern matching on the case class constructor using `Person(name, age)`.
// If the pattern matches, the value of `n` is bound `ada.name` field, and
// `a` is bound to the `ada.age` field.
ada match
case Person(n, a) => println(f"$n is $a years old")
// 2. We can also check only that `ada` is of type `Person` without binding its
// fields by using a `:` pattern. The `:` pattern is used to check if a value is
// of a certain type.
ada match
case p: Person => println(f"${p.name} is ${p.age} years old")
// 3. If we want to both bind the fields and bind a value to the whole instance,
// we can use an `@` pattern.
ada match
case p @ Person(n, a) => println(f"${p.name} is ${a} years old")
package ed
import concurrent.{Await, Future}
import concurrent.duration.Duration
import concurrent.ExecutionContext.Implicits.global
// Pro Tip: you can print the code generated by the Scala compiler after the
// translation of for-comprehensions by running:
//
// scala -Xprint:firstTransform src/main/scala/ed/40056.scala
@main def futuresForTranslation =
val f1 = Future { 451 }
val f2 = Future { 1984 }
val f3 = for v1 <- f1; v2 <- f2 yield v1 + v2
println(Await.result(f3, Duration.Inf))
val f4 = Future { 451 }
val f5 = Future { 1984 }
val f6 = f4.flatMap(v4 => f5.map(v5 => v4 + v5))
println(Await.result(f6, Duration.Inf))
import java.util.concurrent.atomic.AtomicInteger
import collection.parallel.CollectionConverters.*
// This demo should help understanding the `add` method implementation in
// src/main/scala/midterm22/Part8.scala.
// TL;DR: it demonstrate that `+=` is not atomic.
var id: Int = 0
val l =
for _ <- (0 until 10).par yield
id += 1
id
l
l.distinct.size
// Try to execute the worksheet several times (by saving again and again). We
// might get a number of distinct elements not equal to 10. Why?
//
// First, note that `id += 1` is equivalent to `val tmp = id; id = tmp + 1`.
// I.e. the read and write are two separate operations; the `+=` operation is
// not *atomic*.
//
// Then, consider the following execution:
// - T1: reads 0 from id and store it in its local `tmp` var.
// - T2: does the same: reads 0 from id and store it in its local `tmp` var.
// - T1: computes `tmp + 1 == 1`, stores it in `id` and returns `1`
// - T2: computes `tmp + 1 == 1`, stores it in `id` and returns `1`
//
// Now we have two times the ID 1!
// The code we wrote before is equivalent to:
var id2: Int = 0
val l2 =
for _ <- (0 until 10).par yield
val tmp = id2
id2 = tmp + 1
val tmp2 = id2
tmp2
l2
l2.distinct.size // *
// How to make it correct? Use an atomic integer!
val id3 = AtomicInteger()
val l3 =
for _ <- (0 until 10).par yield id3.incrementAndGet()
l3
// Now we are sure that this will always be 10.
l3.distinct.size // *
assert(l3.distinct.size == 10)
// Or, using only what has been seen before the midterm this year: another
// solution is to use synchronize:
var id4: Int = 0
val lock = Object()
val l4 =
for _ <- (0 until 10).par yield lock.synchronized {
id4 += 1
id4
}
l4
// Now we are sure that this will always be 10.
l4.distinct.size // *
assert(l4.distinct.size == 10)
......@@ -19,7 +19,7 @@ class ThreadReturning[A](toRun: => A) extends Thread:
join()
result
def thread[A](toRun: => A): ThreadReturning[A] =
def threadStart[A](toRun: => A): ThreadReturning[A] =
val t = ThreadReturning(toRun)
t.start()
t
......@@ -37,5 +37,5 @@ def countLots: Long =
// Run from the SBT shell with `runMain lecture1.scalaThreadWrapper`.
@main def scalaThreadWrapper: Unit =
val (x, y) = (thread(countLots), thread(countLots))
val (x, y) = (threadStart(countLots), threadStart(countLots))
println((x.joinMe, y.joinMe))
......@@ -7,5 +7,7 @@ package lecture1
trait Task[A]:
def join: A
def task[A](toRun: => A): Task[A] = new Task[A]:
override def join: A = thread(toRun).joinMe
def task[A](toRun: => A): Task[A] =
val t = threadStart(toRun)
new Task[A]:
override def join: A = t.joinMe
package lecture13
import concurrent.{ExecutionContext, Future, Promise}
import concurrent.duration.DurationInt
import akka.actor.{Actor, ActorContext, ActorRef, ActorSystem, Props}
import akka.event.LoggingReceive
import akka.pattern.pipe
import akka.testkit.{ImplicitSender, TestKit}
import akka.util.Timeout
/** This demonstrates a simplified implementation of the ask pattern.
*
* @param promise
* the promise to be completed when a message is received
*/
class AskMiniActor(promise: Promise[Any]) extends Actor:
def receive = LoggingReceive {
case msg => promise.success(msg)
}
extension (receiver: ActorRef)
def ?(msg: Any)(using
// In this simplified implementation, we don't use the timeout.
timeout: Timeout,
// This only used for logging purposes (and to get the actor system in the
// real implementation), but is not used otherwise in the implementation.
sender: ActorRef,
// In the real implementation, the actor system is retrieved differently,
// but here we pass it as an additional implicit parameter for simplicity.
context: ActorContext
): Future[Any] =
context.system.log.debug(
s"Create mini actor to ask $msg from $sender to $receiver"
)
// Create a `Promise` that will be completed when a message is received.
val promise = Promise[Any]()
// Create a mini actor that will complete the `Promise` when it receives a
// message.
val miniActor = context.system.actorOf(Props(AskMiniActor(promise)))
// Send the message to the mini actor, *using the mini actor as the sender*.
// This part is important as it is the mini actor that needs to receive the
// response.
receiver.tell(msg, miniActor)
// Using the `Promise` from the Scala standard library, the corresponding
// `Future` can be retrieved with the method `future`.
promise.future
object Person:
enum Protocol:
case GetName
case GetAge
enum Response:
case Name(name: String)
case Age(age: Int)
case UnknownMessage
class Person(name: String, age: Int) extends Actor:
import Person.Protocol.*
import Person.Response.*
def receive = LoggingReceive {
case GetName => sender() ! Name(name)
case GetAge => sender() ! Age(age)
case _ => sender() ! UnknownMessage
}
object PersonsDatabase:
enum Protocol:
case CreatePerson(name: String, age: Int)
case GetPersonNames(ids: List[Int])
enum Response:
case PersonCreated(id: Int)
case PersonNames(names: List[String])
class PersonsDatabase extends Actor:
import Person.Protocol.*
import Person.Response.*
import PersonsDatabase.Protocol.*
import PersonsDatabase.Response.*
var persons: Map[Int, ActorRef] = Map.empty
var maxId = 0
given ExecutionContext = context.system.dispatcher
given Timeout = Timeout(200.millis)
def receive = LoggingReceive {
case CreatePerson(name, age) =>
val personRef = context.actorOf(Props(Person(name, age)))
persons = persons + (maxId -> personRef)
sender() ! PersonCreated(maxId)
maxId += 1
case GetPersonNames(ids) =>
// We ask every person for their name using the ask pattern. This
// gives us a list of `Future`s that will each be completed with a
// `Name` message.
val rawResponsesFutures: List[Future[Any]] =
ids.map(id => (persons(id) ? GetName))
// We first map each `Future[Any]` to a `Future[Name]` using the
// `mapTo` method. Then, we map each `Future[Name]` to a
// `Future[String]` using the `map` method.
val namesFutures: List[Future[String]] =
rawResponsesFutures.map(_.mapTo[Name].map(_.name))
// We use the `Future.sequence` method to convert a
// `List[Future[String]]` to a `Future[List[String]]`. The resulting
// future will be completed once all the futures in the input list
// are completed.
val futureOfNames: Future[List[String]] = Future.sequence(namesFutures)
// Finally, map the `Future[List[String]]` to a `Future[PersonNames]` and
// pipe it to the sender of the `GetPersonNames` message.
futureOfNames.map(PersonNames.apply).pipeTo(sender())
}
@main def askPatternDemo() =
new TestKit(ActorSystem("DebugSystem")) with ImplicitSender:
import Person.Protocol.*
import Person.Response.*
import PersonsDatabase.Protocol.*
import PersonsDatabase.Response.*
try
val personsDb = system.actorOf(Props(PersonsDatabase()))
personsDb ! CreatePerson("Anita", 20)
expectMsg(PersonCreated(0))
personsDb ! CreatePerson("Berta", 30)
expectMsg(PersonCreated(1))
personsDb ! CreatePerson("Cecilia", 40)
expectMsg(PersonCreated(2))
personsDb ! GetPersonNames(List(0, 1, 2))
expectMsg(PersonNames(List("Anita", "Berta", "Cecilia")))
finally shutdown(system)
package lecture13
import akka.actor.{Actor, ActorContext, ActorRef, ActorSystem, Props}
import akka.event.LoggingReceive
import akka.testkit.{ImplicitSender, TestKit}
class CounterRight extends Actor:
def counter(n: Int): Receive = {
case "increment" => context.become(counter(n + 1), discardOld = false)
// ^^^^^^^^^^^^^^^^^^
// This is needed.
case "get" => sender() ! n
case "decrement" => if n != 0 then context.unbecome()
}
def receive = counter(0)
class CounterWrong extends Actor:
def counter(n: Int): Receive = {
case "increment" => context.become(counter(n + 1))
case "get" => sender() ! n
case "decrement" => if n != 0 then context.unbecome()
}
def receive = counter(0)
@main def becomeDemo() =
new TestKit(ActorSystem("DebugSystem")) with ImplicitSender:
try
val counter = system.actorOf(Props(CounterRight()), "counter")
counter ! "increment"
counter ! "get"
expectMsg(1)
counter ! "increment"
counter ! "get"
expectMsg(2)
counter ! "decrement"
counter ! "get"
expectMsg(1)
// This is wrong if we use CounterWrong because it doesn't set the
// discardOld parameter to false. Therefore, it will not remember the
// previous state and reset the behavior to the initial one, which is
// `counter(0)`.
counter ! "decrement"
counter ! "get"
expectMsg(0)
finally shutdown(system)
package lecture6
import java.util.concurrent.ForkJoinPool
// Run using `running lecture6.ExecutorsCreate`.
@main def ExecutorsCreate =
val executor = new ForkJoinPool
executor.execute(new Runnable:
override def run() = log("This task is run asynchronously.")
)
Thread.sleep(500)
package lecture6
import scala.concurrent.ExecutionContext
@main def ExecutionContextCreate =
val ectx = ExecutionContext.global
ectx.execute(new Runnable:
override def run() = log("This task is run asynchronously.")
)
Thread.sleep(500)
package lecture6
import scala.concurrent.ExecutionContext
def execute(body: => Unit) =
ExecutionContext.global.execute(new Runnable:
override def run() = body
)
@main def SimpleExecute =
execute { log("This task is run asynchronously.") }
Thread.sleep(500)
package lecture6
import scala.concurrent.ExecutionContext
import lecture1.threadStart
class OnePlaceBuffer[Elem]:
var elem: Elem = _
var full = false
def put(e: Elem): Unit = this.synchronized {
while full do wait()
elem = e
full = true
log(f"Put $e in the buffer")
notifyAll()
}
def get(): Elem = this.synchronized {
while !full do wait()
full = false
notifyAll()
log(f"Got $elem from the buffer")
elem
}
@main def OnePlaceBufferDemo =
val buffer = OnePlaceBuffer[Int]()
val getThreads = for i <- 0 until 10 yield threadStart { buffer.get() }
val putThreads = for i <- 0 until 10 yield threadStart { buffer.put(i) }
putThreads.foreach(_.join())
getThreads.foreach(_.join())
package lecture6
class Fork(var index: Int)
def philosopherTurn(i: Int, l: Fork, r: Fork): Unit =
Thread.sleep(10)
var (left, right) =
if i % 2 == 0 then (l, r)
else (r, l)
// if l.index < r.index then (l, r) else (r, l)
left.synchronized {
log(f"Philosopher $i picks up left fork ${left.index}")
right.synchronized {
log(f"Philosopher $i picks up right fork ${right.index} - eating")
Thread.sleep(100)
log(f"Philosopher $i puts down right fork ${right.index}")
}
log(f"Philosopher $i puts down left fork ${left.index}")
}
@main def run() =
val n = 5
val k: Int = 3
val forks = new Array[Fork](n)
val philosophers = new Array[Thread](n)
for p <- 0 to n - 1 do
forks(p) = Fork(p)
for p <- 0 to n - 1 do
philosophers(p) = new Thread:
override def run() =
for _ <- 0 until k do
philosopherTurn(p, forks(p % n), forks((p + 1) % n))
philosophers(p).start
for p <- 0 to n - 1 do
philosophers(p).join()
package lecture6
val log = println(_)
package midterm23
import instrumentation.Monitor
import scala.annotation.tailrec
/** Runs one of the three provided solutions to the barber shop problem.
*
* Run using `sbt "runMain midterm23.barberSolution 2 3"` for example.
*
* @param solutionNumber
* One of the 3 provided solutions to run: 1, 2, or 3.
* @param nCustomers
* The number of customers to simulate.
*/
@main def barberSolution(solutionNumber: Int, nCustomers: Int): Unit =
val barberShop: AbstractBarberShopSolution =
solutionNumber match
case 1 => BarberShopSolution1(nCustomers)
case 2 => BarberShopSolution2(nCustomers)
case 3 => BarberShopSolution3(nCustomers)
barberSolutionMain(barberShop, solutionNumber, nCustomers)
def barberSolutionMain(
barberShop: AbstractBarberShopSolution,
solutionNumber: Int,
nCustomers: Int
): Unit =
val bthread = new Thread:
override def run() =
barberShop.barber()
bthread.start()
val customerThreads =
for i <- 1 to nCustomers yield
val cthread = new Thread:
override def run() =
barberShop.customer(i)
cthread.start()
cthread
bthread.join()
customerThreads.foreach(_.join())
abstract class AbstractBarberShopSolution:
// var notifyBarber: Boolean = false
// var notifyCustomer: Boolean = false
// val lockObj = Object()
// In order to be able to mock the three attributes above in the tests, we
// replace them with three private attributes and public getters and setters.
// Without overrides, this is equivalent to the definitions above.
private var _notifyBarber: Boolean = false
private var _notifyCustomer: Boolean = false
private val _lockObject: Monitor = new Monitor {}
def notifyBarber = _notifyBarber
def notifyBarber_=(v: Boolean) = _notifyBarber = v
def notifyCustomer = _notifyCustomer
def notifyCustomer_=(v: Boolean) = _notifyCustomer = v
def lockObj: Monitor = _lockObject
def hairCut(): Unit =
log("start hair cut")
Thread.sleep(1000)
log("finish hair cut")
def customer(n: Int): Unit
def barber(): Unit
/** Logs a message to the console.
*
* Overridden in the tests to avoid printing gigabytes of output.
*
* @param s
*/
def log(s: String) = println(s)
/** This is the solution we expected for the `customer` method.
*
* However, now that you have learned about `@volatile`, you can see that the
* `customer` method is not thread-safe. Can you spot a problem with this
* solution?
*
* Note that the `log` calls were added to help you understand what is going
* on. They are not part of the expected solution.
*/
trait BarberShopCustomerSolution1 extends AbstractBarberShopSolution:
override def customer(n: Int) =
lockObj.synchronized {
log(f"customer ${n} sits on the chair and waits for the barber")
notifyBarber = true
while !notifyCustomer do {}
notifyCustomer = false
log(f"customer ${n} leaves the chair")
}
/** This is the solution we expected for the `barber` method.
*
* Same question as above: can you spot a problem with this solution?
*
* @param customersNumber
* The number of customers to simulate.
*/
trait BarberShopBarberSolution1(customersNumber: Int)
extends AbstractBarberShopSolution:
override def barber() =
// while true do
// We replace the infinite loop from the instructions with a loop doing a
// limited number of iterations so that we can test the code.
for _ <- 1 to customersNumber do
log("barber waits for a customer")
while !notifyBarber do {}
notifyBarber = false
hairCut()
notifyCustomer = true
log("barber stops working")
class BarberShopSolution1(customersNumber: Int)
extends BarberShopCustomerSolution1,
BarberShopBarberSolution1(customersNumber)
/** The problem with BarberShopSolution1 is that the `notifyBarber` and
* `notifyCustomer` are accessed from multiple threads without any
* synchronization. This means that the compiler is free to reorder the
* instructions in the `customer` and `barber` methods, which can lead to
* unexpected behaviors like infinite loops!
*
* To solve this problem, the solution below redefines the two attributes as
* `@volatile`.
*
* @param customersNumber
* The number of customers to simulate.
*/
class BarberShopSolution2(customersNumber: Int)
extends BarberShopSolution1(customersNumber):
@volatile private var _notifyBarber: Boolean = false
@volatile private var _notifyCustomer: Boolean = false
override def notifyBarber = _notifyBarber
override def notifyBarber_=(v: Boolean) = _notifyBarber = v
override def notifyCustomer = _notifyCustomer
override def notifyCustomer_=(v: Boolean) = _notifyCustomer = v
/** The solution below uses a different approach to solve the problem of
* BarberShopSolution1. Instead of using `@volatile`, it uses the lock object
* to synchronize the accesses to the two attributes. Note how each access to
* the attributes is now wrapped in a `lockObj.synchronized` block.
*
* @param customersNumber
* The number of customers to simulate.
*/
class BarberShopSolution3(customersNumber: Int)
extends AbstractBarberShopSolution:
override def customer(n: Int) =
getHairCut(n)
@tailrec
private def getHairCut(n: Int): Unit =
val sited = lockObj.synchronized {
if notifyBarber then
false
else if notifyCustomer then
false
else
log(f"customer ${n} sits on the chair and waits for the barber")
notifyBarber = true
true
}
if sited then
while !lockObj.synchronized { notifyCustomer } do {}
lockObj.synchronized {
log(f"customer ${n} leaves the chair")
notifyCustomer = false
}
else
getHairCut(n)
override def barber() =
for _ <- 1 to customersNumber do
log("barber waits for a customer")
while !lockObj.synchronized { notifyBarber } do {}
hairCut()
lockObj.synchronized {
notifyCustomer = true
notifyBarber = false
}
log("barber stops working")
package midterm23
import java.util.concurrent.atomic.AtomicInteger
/** This is an example `BarberShopStudentAnswer`, like the one we made for your
* own solution and that you can find on Moodle. The solution below is boringly
* correct as it use the same implementation as the expected solution. See
* BarberShopSolution.scala for explanations and comments.
*
* @param customersNumber
* The number of customers to simulate.
*/
class BarberShopStudentAnswer333(customersNumber: Int)
extends AbstractBarberShopSolution:
/** This attribute is used to count the number of hair cuts. It is needed for
* testing because we do not want to loop forever in the `barber` method!
*/
val hairCutsCounter = AtomicInteger(customersNumber)
override def customer(n: Int) =
lockObj.synchronized {
notifyBarber = true
while !notifyCustomer do {}
notifyCustomer = false
}
override def barber() =
// while true
// We replace the infinite loop from the instructions with a loop doing a
// limited number of iterations so that we can test the code.
while hairCutsCounter.get() != 0 do
while !notifyBarber do {}
notifyBarber = false
hairCut()
hairCutsCounter.decrementAndGet() // You can ignore this line. It has been added for testing.
notifyCustomer = true
/** Version of `BarberShopStudentAnswer333` with the methods `customer`
* overridden to use the expected implementation.
*
* @param n
* The number of customers to simulate.
*/
class BarberShopStudentAnswer333WithCorrectCustomer(n: Int)
extends BarberShopStudentAnswer333(n)
with BarberShopCustomerSolution1
/** Version of `BarberShopStudentAnswer333` with the methods `barber` overridden
* to use the expected implementation.
*
* @param n
* The number of customers to simulate.
*/
class BarberShopStudentAnswer333WithCorrectBarber(n: Int)
extends BarberShopStudentAnswer333(n)
with BarberShopBarberSolution1(n)
// To grade, we ran tests with each of these classes.