Pro Yearly is on sale from $80 to $50! »

Asynchronicity

Efaab4ba42d459365cdd2534ab280c86?s=47 Marcus
October 20, 2016

 Asynchronicity

talk at PHASE in the Integrichain office. October 2016.

Efaab4ba42d459365cdd2534ab280c86?s=128

Marcus

October 20, 2016
Tweet

Transcript

  1. Asynchronicity Actors together with Futures @dreadedsoftware | @integrichain

  2. Computation Return Value Duration Arguments @dreadedsoftware | @integrichain

  3. Abstracting over Duration. I don’t care how this is done

    Scala does it for me!!! @dreadedsoftware | @integrichain
  4. Futures • Provides meaningful abstractions for • decoupling duration from

    computation • performing multiple computations in the same duration • composing computations through adjacent durations • combining results without respect to duration @dreadedsoftware | @integrichain
  5. Take factorial def simple(n: Int): BigInt = { @annotation.tailrec def

    recurse(n: Int, acc: BigInt): BigInt = { if(1 < n) recurse(n - 1, n * acc) else acc } val result = recurse(n, 1) result } @dreadedsoftware | @integrichain
  6. This takes a really long time for reasonable values! 100!

    ~ 1ms 10,000! ~ 70ms 1,000! ~ 4ms 10000! ~ 7000ms And it scales like sh*t! @dreadedsoftware | @integrichain
  7. Let’s make it Asynchronous! def naiveAsync (n: Int) (implicit ec:

    ExecutionContext): Future[BigInt] = { Future{simple(n)} } All Future computations need one of these! The new Free Lunch @dreadedsoftware | @integrichain
  8. This takes a really long time for reasonable values! 100!

    ~ 1ms 10,000! ~ 70ms 1,000! ~ 4ms 100,000! ~ 7000ms At least its in the background… 1,000,000! ~ ? (killed at 30mins) @dreadedsoftware | @integrichain
  9. We can do better! Multiplication is associative! a * b

    * c * d = (a * b) * (c * d) @dreadedsoftware | @integrichain
  10. Split up the factorial function def partialFactorial(low: Int, high: Int):

    BigInt = { @annotation.tailrec def recurse( low: Int, high: Int, acc: BigInt ): BigInt = { if(low <= high) recurse(low, high - 1, high * acc) else acc } recurse(low, high, 1) } def identity: BigInt = 1 def sync(n: Int): BigInt = { getBuckets(n). foldLeft(identity){(acc, bounds) => partialFactorial(bounds._1, bounds._2) * acc } } Takes 10k at a time, trust me ^_^! @dreadedsoftware | @integrichain
  11. This, even without Futures, is far better than before! 100!

    ~ 2ms 10,000! ~ 70ms 1,000! ~ 4ms 100,000! ~ 1000ms Still will not scale! 1,000,000! ~ 110,000ms @dreadedsoftware | @integrichain
  12. And try to convert it directly. Let’s start with our

    synchronous code def identity: BigInt = 1 def sync(n: Int): BigInt = { getBuckets(n). foldLeft(identity){(acc, bounds) => partialFactorial(bounds._1, bounds._2) * acc } } def identity: Future[BigInt] = ??? def async1( n: Int )(implicit ec: ExecutionContext): Future[BigInt] = { getBuckets(n).foldLeft(identity){(acc, bounds) => ??? } } @dreadedsoftware | @integrichain
  13. We have issues. •We need an identity •For BigInt it

    was 1 •For Future[BigInt]? •We need a way to combine values •For BigInt it was * •For Future[BigInt]? @dreadedsoftware | @integrichain
  14. Future.successful • Wraps a value in a Future • If

    1 is identity for BigInt; Future.successful(1) is identity for Future[BigInt] Future has map Future has flatMap • Futures can be combined using for comprehensions • If * combines BigInt values; * inside a for can combine Future[BigInt] values @dreadedsoftware | @integrichain
  15. Now we really are Asynchronous! def async2( n: Int )(implicit

    ec: ExecutionContext): Future[BigInt] = { val identityF = Future.successful(identity) val buckets: Seq[(Int, Int)] = getBuckets(n) buckets.foldLeft(identityF){(acc, n) => for{ a <- acc b <- Future{partialFactorial(n._1, n._2)} }yield{a * b} } } Sequential! Same runtime characteristics as last attempt… @dreadedsoftware | @integrichain
  16. When done well def async3( n: Int)( implicit ec: ExecutionContext):

    Future[BigInt] = { val identityF = Future.successful(identity) val buckets: Seq[(Int, Int)] = getBuckets(n) buckets.foldLeft(identityF){(acc, n) => val next = Future{partialFactorial(n._1, n._2)} for{ a <- acc b <- next }yield{a * b} } } When we were synchronous we had: 1,000,000! ~ 110,000ms Now we have: 1,000,000! ~ 105,000ms Still Sequential! @dreadedsoftware | @integrichain
  17. Our friend Algebra saves us again. Multiplication is commutative! a

    * b * c * d = a * d * b * c @dreadedsoftware | @integrichain
  18. We can multiply these in any order we want! Why

    not just do right half and left half? def async( n: Int)( implicit ec: ExecutionContext): Future[BigInt] = { val start = System.currentTimeMillis val id = Future.successful(identity) val buckets: Seq[(Int, Int)] = getBuckets(n) val partials: Seq[Future[BigInt]] = buckets.map{ case (low, high) => Future{partialFactorial(low, high)} } bigFuture(partials) } Step1: Accumulate Futures @dreadedsoftware | @integrichain
  19. Determining what you have @annotation.tailrecdef bigFuture( s: Seq[Future[BigInt]])( implicit ec:

    ExecutionContext): Future[BigInt] = { val id = Future.successful(identity) if(1 < s.size){ bigFuture(collapse(s)) }else if(1 == s.size){ s.head }else id } Step2: Switch on size Recurseif needed @dreadedsoftware | @integrichain
  20. Splitting the pot def collapse( seq: Seq[Future[BigInt]])( implicit ec: ExecutionContext):

    Seq[Future[BigInt]] = { val grouped: Seq[Seq[Future[BigInt]]] = seq.grouped(2).toSeq grouped.map{seq: Seq[Future[BigInt]] => val fut: Future[Seq[BigInt]] = Future.sequence(seq) fut.map{seq => seq.tail.fold(seq.head)((a, b) => a*b) } } } Step3: Two by Two @dreadedsoftware | @integrichain
  21. The performance here is astounding! 1,000,000! ~ 9500ms I only

    have 4 cores and was able to get a > 10x speed up! @dreadedsoftware | @integrichain
  22. So far… •We has a linear computation •We broke it

    up into smaller computations •Wrapped the smaller computations in Futures •Broke them up into a hierarchy @dreadedsoftware | @integrichain
  23. This was a lot of work. Is there a better

    abstraction we could have used? @dreadedsoftware | @integrichain
  24. Actors • Meant to asynchronously perform small computations • Compose

    hierarchically by design • No need for recursive inner function • Pass messages for communication; no direct access • Data protection • Unfortunately, typeless • Compiler can’t help us here • We can gain confidence through discipline @dreadedsoftware | @integrichain
  25. Is factorial a good candidate for Actors? Small computations Hierarchy

    @dreadedsoftware | @integrichain
  26. Factorial is a bad candidate for Actors. Forced compliance. @dreadedsoftware

    | @integrichain
  27. Don’t worry! Our example can still be used! @dreadedsoftware |

    @integrichain
  28. n choose k Recall: n! . n choose k =

    k!(n-k)! This is hierarchial: • 3 parts: a = n!, b = k!, c = (n-k)! • 2 level hierarchy: d = b * c, result = a / d Composed of factorials which we’ve already made async! @dreadedsoftware | @integrichain
  29. Make messages from these Some design • Numerator • Denominator

    • Left operand • Right operand case class Numerator(`n!`: BigInt) case class Denom(`k!(n-k)!`: BigInt) case class DenomLeft(`k!`: BigInt) case class DenomRight(`(n-k)!`: BigInt) @dreadedsoftware | @integrichain
  30. And a helper private def doFactorial( n: Int)( onSuccess: BigInt

    => Unit)( onFailure: Throwable => Unit)( implicit ec: ExecutionContext): Future[BigInt] = { factorial.async(n).andThen{ case Success(value) => onSuccess(value) case Failure(th) => onFailure(th) } } @dreadedsoftware | @integrichain
  31. class Combinations extends Actor{ implicit val ec: ExecutionContext = context.dispatcher

    override def receive: Receive = { case (n: Int, k: Int) => val sentBy = sender() val diff = n – k if(n < 1) nothingToDo(sentBy) else{ perform(n, k, sentBy) context.become(compute(sentBy)) } case message @ _ => //failure println(message) context.stop(self) } private def nothingToDo(sentBy: ActorRef) = sentBy ! (0: BigInt) … } For the Futures Input n and k The Trivial Case Let’s dive in here! Let it crash! (After logging of course) @dreadedsoftware | @integrichain
  32. class Combinations extends Actor{ … private def perform( n: Int,

    k: Int, sentBy: ActorRef) = { val `n-k` = n – k if(`n-k` < 0) nothingToDo(sentBy) else{ doFactorial(n)( self ! Numerator(_))( self ! _.toString()) } context.actorOf(Props(new Denominator)) ! (k, `n-k`) } … } The Trivial Case Sucessfuln! Report value. Call for the denominator to be computed @dreadedsoftware | @integrichain
  33. class Combinations extends Actor{ … private var numerator: Option[BigInt] =

    None private var denominator: Option[BigInt] = None private def compute(sentBy: ActorRef): Receive = { case Numerator(n) => denominator.fold{ numerator = Some(n)}{denominator => sentBy ! (n / denominator) context.stop(self) } case Denom(d) => numerator.fold{ denominator = Some(d)}{numerator => sentBy ! (numerator / d) context.stop(self) } case message @ _ => //failure println(message) context.stop(self) } } var is not my favorite way to do this; it works Will see another method later. No denominator, continue on. Have denominator, ready! Let it crash! No numerator, continue on. Have numerator, ready! @dreadedsoftware | @integrichain
  34. That’s the main Actor • Accepts n and k •

    Dispatches numerator computation to a Future • Dispatches denominator computation to a child Actor • Combines results of numerator and denominator before terminating @dreadedsoftware | @integrichain
  35. private class Denominator extends Actor{ … override def receive: Receive

    = { case (k: Int, diff: Int) => val `n-k` = diff val sentBy = sender() context.actorOf(Props(new `k!`)) ! k context.actorOf(Props(new `(n-k)!`)) !`n-k` context.become(hasNone(sentBy)) case message @ _ => context.parent ! Message } … } Dispatch left part of computation Let it crash! Dispatch right part of computation Using become to change state @dreadedsoftware | @integrichain
  36. private class Denominator extends Actor{ … private def hasNone(ref: ActorRef):

    Receive = { case DenomLeft(a) => context.become(hasLeft(a, ref)) case DenomRight(b) =>context.become(hasRight(b, ref)) case message @ _ => fail(message) } private def hasLeft( a: BigInt, sentBy: ActorRef): Receive = { case DenomRight(b) => ref ! Denom(a * b) case message @ _ =>fail(message) } private def hasRight( b: BigInt, ref: ActorRef): Receive = { case DenomLeft(a) => ref ! Denom(a * b) case message @ _ => fail(message) } private def fail(m: Message){ context.parent ! M context.stop(self) } } Current state determines next state. Each state can only receive messages that will put it into the next good state. Duplication of messages will trigger an error. No var! @dreadedsoftware | @integrichain
  37. So • Futures are the standard for asynchronous computation •

    Especially in the linear case • The payoffs can be enormous • Actors are for larger concurrent problems • They help organize the problem into a hierarchy • Use discretion when deploying, not necessarily a good idea @dreadedsoftware | @integrichain