-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature request: deferred-start Future
s
#108
Comments
Discussed today with @natsukagami, @odersky and @EugeneFlesselle. We could use //> using dep "ch.epfl.lamp::gears::0.2.0"
import scala.concurrent.duration.given
import gears.async.{Future, Async, AsyncOperations}
import gears.async.default.given
import Async.await
final class AsyncLazy[T](body: (Async, Async.Spawn) ?=> T)(using Async, Async.Spawn):
lazy val future: Future[T] = Future(body)
def get(): T = future.await
@main def main =
Async.blocking:
println("starting")
val a = new Array[AsyncLazy[Int]](4)
a(0) = AsyncLazy:
println("compute a(0)")
a(1).get() + a(2).get()
a(1) = AsyncLazy:
println("compute a(1)")
a(3).get() + 4
a(2) = AsyncLazy:
println("compute a(2)")
a(3).get() + 2
a(3) = AsyncLazy:
println("compute a(3)")
1
println("initialized")
println(a(0).get()) That's nicer than what I have above, but it still requires extra locking/synchronization compared to what we would get with #109. Another drawback of the "lazy" approach in both snippets here where |
Discussed today with @vkuncak. For this simple example, we can also tie the knot using a //> using dep "ch.epfl.lamp::gears::0.2.0"
import scala.concurrent.duration.given
import gears.async.{Future, Async, AsyncOperations}
import gears.async.default.given
import Async.await
final class AsyncLazy[T](body: (Async, Async.Spawn) ?=> T)(using Async, Async.Spawn):
lazy val future: Future[T] = Future(body)
def get(): T = future.await
@main def main =
Async.blocking:
println("starting")
lazy val a: IArray[AsyncLazy[Int]] =
IArray.tabulate(4):
case 0 => AsyncLazy:
println("compute a(0)")
a(1).get() + a(2).get()
case 1 => AsyncLazy:
println("compute a(1)")
a(3).get() + 4
case 2 => AsyncLazy:
println("compute a(2)")
a(3).get() + 2
case 3 => AsyncLazy:
println("compute a(3)")
1
println("initialized")
println(a(0).get()) Or we can define each task as //> using dep "ch.epfl.lamp::gears::0.2.0"
import scala.concurrent.duration.given
import gears.async.{Future, Async, AsyncOperations}
import gears.async.default.given
import Async.await
@main def main =
Async.blocking:
println("starting")
lazy val a0 = Future:
println("compute a(0)")
a1.await + a2.await
lazy val a1 = Future:
println("compute a(1)")
a3.await + 4
lazy val a2 = Future:
println("compute a(2)")
a3.await + 2
lazy val a3 = Future:
println("compute a(3)")
1
println("initialized")
println(a0.await) |
There may be locking that is much more global than what is desired in the above examples. |
Right, one should use //> using dep "ch.epfl.lamp::gears::0.2.0"
import scala.concurrent.duration.given
import gears.async.{Future, Async, AsyncOperations}
import gears.async.default.given
import Async.await
@main def main =
Async.blocking:
println("starting")
lazy val a0 = Future:
trace("a0"):
a1.await + a2.await
lazy val a1 = Future:
trace("a1"):
AsyncOperations.sleep(3.seconds)
a3.await + 4
lazy val a2 = Future:
trace("a2"):
AsyncOperations.sleep(3.seconds)
a3.await + 2
lazy val a3 = Future:
trace("a3"):
1
println("initialized")
time:
println(List(a0, a1, a2, a3).awaitAll)
def trace[T](name: String)(body: => T): T =
println(s"start $name")
val res = body
println(s"end $name")
res
def time[T](body: => T): T =
val start = System.currentTimeMillis() / 1_000.0
val res = body
val end = System.currentTimeMillis() / 1_000.0
println(f"${end - start}%.0f seconds")
res This runs in 3 seconds. Better, isn't it? |
I agree that @mbovel 's solution is the most correct one, though it would be nice not to have to write
I was thinking we could do something like a @main def main =
Async.blocking:
println("starting")
Async.deferrable:
val a0 = Future.deferred:
trace("a0"):
a1.await + a2.await
val a1 = Future.deferred:
trace("a1"):
AsyncOperations.sleep(3.seconds)
a3.await + 4
val a2 = Future.deferred:
trace("a2"):
AsyncOperations.sleep(3.seconds)
a3.await + 2
val a3 = Future.deferred:
trace("a3"):
1
a0.await // .await called, start all futures (note that if a non-deferred This would solve the problem of having to |
Could we add a
Future
constructor in the gears library that allows computations to be defined without starting them immediately?This would enable defining a DAG of inter-dependent asynchronous tasks in an arbitrary order. For example, in pseudo-code:
The key requirement here is that tasks should not execute until explicitly triggered, allowing dependencies to be set up first.
Below is a working user-land implementation:
Running this code produces the following output:
I believe a dedicated feature in gears could achieve something similar more efficiently, by saving the extra synchronization.
A
Future
constructor could return a deferred-startFuture
, of which the execution begins only when explicitly triggered. In the example above, one could then loop overFuture
s to start them after they have all been defined.The text was updated successfully, but these errors were encountered: