Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Asynchronicity

Marcus
October 20, 2016

 Asynchronicity

talk at PHASE in the Integrichain office. October 2016.

Marcus

October 20, 2016
Tweet

More Decks by Marcus

Other Decks in Programming

Transcript

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

    Scala does it for me!!! @dreadedsoftware | @integrichain
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. We can do better! Multiplication is associative! a * b

    * c * d = (a * b) * (c * d) @dreadedsoftware | @integrichain
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. Our friend Algebra saves us again. Multiplication is commutative! a

    * b * c * d = a * d * b * c @dreadedsoftware | @integrichain
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. This was a lot of work. Is there a better

    abstraction we could have used? @dreadedsoftware | @integrichain
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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