Petra Bierleutgeb
@pbvie
(a.k.a. agenda)
Why do we need an ExecutionContext every time we use Future?
...and we don't like to wait
The term Concurrency refers to techniques that make programs more usable. If you can load multiple documents simultaneously in the tabs of your browser and you can still open menus and perform other actions, this is concurrency.
https://wiki.haskell.org/Parallelism_vs._Concurrency
"
"
The term Parallelism refers to techniques to make programs faster by performing several computations at the same time. This requires hardware with multiple processing units.
https://wiki.haskell.org/Parallelism_vs._Concurrency
"
"An asynchronous operation is an operation which continues in the background after being initiated, without forcing the caller to wait for it to finish before running other code."
https://blog.slaks.net/2014-12-23/parallelism-async-threading-explained/
A whirlwind tour of threads and friends
class MyThread extends Thread {
override def run(): Unit = {
// do async work
}
}
val t = new MyThread()
t.start()
A Scala developer's concurrency toolbox
...a future is a placeholder object for a value that may be become available at some point in time, asynchronously. It is not the asynchronous computation itself.
"
// scala.concurrent.Future
def apply[T](body: =>T)(implicit executor: ExecutionContext): Future[T]
// scala.concurrent.Future
def apply[T](body: =>T)(implicit executor: ExecutionContext): Future[T]
// scala.concurrent.Future
def apply[T](body: =>T)(implicit executor: ExecutionContext): Future[T]
...It is not the asynchronous computation itself.
"Create a placeholder object that may contain the result of the given expression once it is completed...or an error"
val f: Future[Int] = promiseSample()
def promiseSample(): Future[Int] = {
val p = Promise[Int]
someLongRunningOp(p)
p.future
}
def someLongRunningOp(p: Promise[Int]): Future[Unit] = Future {
Thread.sleep(5000) // do something slow
p.success(5)
}
implemented using Promise
What is it and what do we need it for?
// java.util.concurrent.Executor
public interface Executor {
void execute(Runnable command);
}
trait ExecutionContext {
def execute(runnable: Runnable): Unit
def reportFailure(cause: Throwable): Unit
}
=> we can't break that law
that's our execution context
From Future.apply to running on an EC
Future {
2 + 3
}
Future[Int]
Cannot find an implicit ExecutionContext. You might pass
an (implicit ec: ExecutionContext) parameter to your method
or import scala.concurrent.ExecutionContext.Implicits.global.
import scala.concurrent.ExecutionContext.Implicits.global
Future {
2 + 3
}
Future.apply(2 + 3)
def apply[T](body: =>T)(implicit executor: ExecutionContext): Future[T] =
unit.map(_ => body)
Future.apply(2 + 3)
def apply[T](body: =>T)(implicit executor: ExecutionContext): Future[T] =
unit.map(_ => body)
val unit: Future[Unit] = successful(())
def successful[T](result: T): Future[T] = Promise.successful(result).future
// scala.concurrent.Promise
def successful[T](result: T): Promise[T] = fromTry(Success(result))
def fromTry[T](result: Try[T]): Promise[T] =
impl.Promise.KeptPromise[T](result)
KeptPromise.Successful(Success(()))
Future.apply(2 + 3)
def apply[T](body: =>T)(implicit executor: ExecutionContext): Future[T] =
unit.map(_ => body)
def map[S](f: T => S)(implicit executor: ExecutionContext): Future[S] =
transform(_ map f)
// scala.concurrent.impl.Promise
override def transform[S](f: Try[T] => Try[S])
(implicit executor: ExecutionContext): Future[S] = {
val p = new DefaultPromise[S]()
onComplete { result =>
p.complete(try f(result) catch { case NonFatal(t) => Failure(t) })
}
p.future
}
val p = new DefaultPromise[S]()
onComplete { result =>
p.complete(try f(result) catch { case NonFatal(t) => Failure(t) })
}
p.future
// scala.concurrent.impl.Promise.KeptPromise.Kept
override def onComplete[U](func: Try[T] => U)
(implicit executor: ExecutionContext): Unit =
(new CallbackRunnable(executor.prepare(), func)).executeWithValue(result)
returned to caller immediately
`this` is our KeptPromise with result Success(())
2. Runnable.run takes no params and returns nothing
=> how do we get data in and out?
// scala.concurrent.impl.CallbackRunnable
def executeWithValue(v: Try[T]): Unit = {
require(value eq null) // can't complete it twice
value = v
try executor.execute(this) catch { case NonFatal(t) => executor reportFailure t }
}
// scala.concurrent.impl.CallbackRunnable
override def run() = {
require(value ne null) // must set value to non-null before running!
try onComplete(value) catch { case NonFatal(e) => executor reportFailure e }
}
this is our EC
result =>
p.complete(try f(result) catch { case NonFatal(t) => Failure(t) })
(t: Try[Unit]) => t.map(_ => 2 + 3)
Success(())
Future(1) // don't do this
Don't use Future.apply to wrap already available values
It will trigger the whole chain of transform, create a Runnable and execute the expression on the given EC
// way better!
Future.successful(1)
...it will create a KeptPromise without all the overhead
val f: Future[Int] =
Future(2 + 3).map(r => r + 1).map(r => r * 5).map(r => r - 1)
val f: Future[Int] =
Future(2 + 3).map(r => r + 1).map(r => r * 5).map(r => r - 1)
// implementation of Promise.future
def future: this.type = this
The Promise and the returned Future are actually the same instance
final def onComplete[U](func: Try[T] => U)(implicit executor: ExecutionContext): Unit =
dispatchOrAddCallback(new CallbackRunnable[T](executor.prepare(), func))
@tailrec
private def dispatchOrAddCallback(runnable: CallbackRunnable[T]): Unit = {
get() match {
case r: Try[_] => ... // promise is already completed => execute callback
case dp: DefaultPromise[_] => ... // result is another DP (i.e. when using flatMap)
case listeners: List[_] => ... // promise is not completed => add callback to list
}
}
our ExecutionContext
will be called when this Promise is completed
final def tryComplete(value: Try[T]): Boolean = {
val resolved = resolveTry(value)
tryCompleteAndGetListeners(resolved) match {
...
case rs => rs.foreach(r => r.executeWithValue(resolved)); true
}
}
execute stored callbacks with
result of this Promise
a.k.a. Scala's default ExecutionContext
divide and conquer!
def doSomethingAsync()(ec: ExecutionContext): Future[Int] = ...
implicit val ec: ExecutionContext = ExecutionContext.global
Future.traverse(1 to 25)(i => doSomethingAsync())
=> depends on configuration
fork in run := true
javaOptions += "-Dscala.concurrent.context.maxThreads=3"
waiting for Task 4
waiting for Task 3
Queue
maxThreads = 2
def slowOp(a: Int, b: Int)(implicit ec: ExecutionContext): Future[Int] = Future {
logger.debug("slowOp")
Thread.sleep(80)
a + b
}
def combined(a: Int, b: Int)(implicit ec: ExecutionContext): Future[Int] = {
val seqF = for (i <- 1 to 100) yield slowOp(a + i, b + i + 1)
Future.sequence(seqF).map(_.sum)
}
0.950 ops/s
[DEBUG] Thread[20,scala-execution-context-global-20,5] - slowOp {}
[DEBUG] Thread[18,scala-execution-context-global-18,5] - slowOp {}
[DEBUG] Thread[15,scala-execution-context-global-15,5] - slowOp {}
[DEBUG] Thread[17,scala-execution-context-global-17,5] - slowOp {}
[DEBUG] Thread[22,scala-execution-context-global-22,5] - slowOp {}
[DEBUG] Thread[16,scala-execution-context-global-16,5] - slowOp {}
[DEBUG] Thread[19,scala-execution-context-global-19,5] - slowOp {}
[DEBUG] Thread[21,scala-execution-context-global-21,5] - slowOp {}
[DEBUG] Thread[18,scala-execution-context-global-18,5] - slowOp {}
[DEBUG] Thread[15,scala-execution-context-global-15,5] - slowOp {}
[DEBUG] Thread[20,scala-execution-context-global-20,5] - slowOp {}
[DEBUG] Thread[17,scala-execution-context-global-17,5] - slowOp {}
[DEBUG] Thread[16,scala-execution-context-global-16,5] - slowOp {}
def slowOpBlocking(a: Int, b: Int)(implicit ec: ExecutionContext): Future[Int] = Future {
logger.debug("slowOpBlocking")
blocking {
Thread.sleep(80)
}
a + b
}
def combined(a: Int, b: Int)(implicit ec: ExecutionContext): Future[Int] = {
val seqF = for (i <- 1 to 100) yield slowOpBlocking(a + i, b + i + 1)
Future.sequence(seqF).map(_.sum)
}
6.861 ops/s
...
[DEBUG] Thread[3177,scala-execution-context-global-3177,5] - slowOpBlocking {}
[DEBUG] Thread[3178,scala-execution-context-global-3178,5] - slowOpBlocking {}
[DEBUG] Thread[3179,scala-execution-context-global-3179,5] - slowOpBlocking {}
[DEBUG] Thread[3180,scala-execution-context-global-3180,5] - slowOpBlocking {}
[DEBUG] Thread[3181,scala-execution-context-global-3181,5] - slowOpBlocking {}
[DEBUG] Thread[3182,scala-execution-context-global-3182,5] - slowOpBlocking {}
[DEBUG] Thread[3183,scala-execution-context-global-3183,5] - slowOpBlocking {}
[DEBUG] Thread[3184,scala-execution-context-global-3184,5] - slowOpBlocking {}
...
Thank you!
Questions?