Marcus
October 20, 2016

# Asynchronicity

talk at PHASE in the Integrichain office. October 2016.

October 20, 2016

## Transcript

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

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