-
Notifications
You must be signed in to change notification settings - Fork 27
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
Experiment: Only allow Async.Spawn
to spawn runnable futures
#46
Conversation
The goal is to disallow spawning dangling Futures from `using Async` functions. `Async.Spawnable` is an opaque alias of `Async`, defined as a subtype of `Async`, obtained by explicitly "upgrading" it through `Async.spawning` (which works similarly to `Async.group`, so futures are all cancelled after) -- or automatically given through `Async.blocking` or `Future.apply`. The `Async.Spawnable`-taking functions (signalling usage of dangling futures) should follow the hacky signature of `Future.apply`: > def apply[T](body: Async.Spawnable ?=> T) > (using async: Async, spawn: Async.Spawnable & async.type): T to ensure that the given `Async` instance (which is usually synthesized to be the innermost context) is the same instance as the `Async.Spawnable` instance. It happens quite often (especially when nesting `Async` contexts) that these don't match: > extension[T] (s: Seq[T]) > def parallelMap[U](f: T => Async ?=> U)(using Async): Seq[U] > > Async.blocking: // Async.Spawnable here... > val seq = Seq(1, 2, 3, 4, 5) > .parallelMap: n => // Async here... > Async.select( > Future(doSomethingAsync(n)) handle Some(_), // oops, spawned by Async.blocking > Future(Async.sleep(1.minute)) handle _ => None, // oops, spawned by Async.blocking > ) > // oops, leaking all the futures... with the `Future.apply` signature as above, this does not happen and will give a compile time error.
Not really related to the PR, rather to the general design - out of curiosity, why do you need the context parameter in In ox we have a similar method, but the signature is simpler (it's a top-level function, not an extension, but that's irrelevant I guess): |
The context parameter usually appears if as a function you intend to
provide a different async scope to the function parameter.
In this case, we would want `f`s to run in Futures within the body.
Runnable futures wrap and pass a new Async context to its body, handling
cancellation and provide its own scoping.
Perhaps `parallelMap` want to cancel the spawned futures for some reason,
it cannot do so if `f` were to capture the Async instance from outside.
…On Wed, Feb 28, 2024 at 12:25 PM Adam Warski ***@***.***> wrote:
Not really related to the PR, rather to the general design - out of
curiosity, why do you need the context parameter in def parallelMap[U](f:
T => Async ?=> U)(using Async): Seq[U]? If f would spawn any async
computations, it could capture the capability from the enclosing
environment, at usage-site, no?
In ox we have a similar method, but the signature is simpler (it's a
top-level function, not an extension, but that's irrelevant I guess): def
mapPar[I, O, C[E] <: Iterable[E]](parallelism: Int)(iterable: =>
C[I])(transform: I => O): C[O]. So I think we avoid this problem (if I
understand the problem correctly), but maybe we have some other problems
that I don't know about ;)
—
Reply to this email directly, view it on GitHub
<#46 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACFEK2MM4WQICCJP2BSGXA3YV4H3XAVCNFSM6AAAAABD4L4LEOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTSNRYG43TSMBZHA>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
Ah yes I see the use-case and problem :) I guess I would typically expect myList.parallelMap { n =>
supervised { // custom scope
val f1 = fork(...)
val f2 = fork(...)
f1.join() + f2.join()
}
} That way if a particular mapping invocation is interrupted (e.g. one of the But still it's possible that when supervised {
myList.parallelMap { n =>
val f1 = fork(...)
val f2 = fork(...)
f1.join() + f2.join()
}
} Now an exception thrown by any of the But I guess that's what you are trying to solve here, in another way? One thing I don't understand - isn't |
They are quite different from
Initially I think it is totally fine to keep both of the capabilities within I don't think there is a concept of
With the above in mind I think it is more clear that |
There is not that much value in *not* giving `Spawnable` within `Async.group`, so we should just merge the two together.
@natsukagami Thanks, a great explanation! Indeed, that's where ox/gears differ: in ox, there's no capability needed to block (suspend) - you can always do that. So we only have the other one (scoping threads). |
Should make it more compatible with capability-speak.
... to be contrasted with Threads and be more in line with gear's Timer and Multiplexer APIs.
Async.Spawnable
to spawn runnable futuresAsync.Spawnable
to spawn runnable futures
Async.Spawnable
to spawn runnable futuresAsync.Spawn
to spawn runnable futures
Co-authored-by: Maximilian Müller <m8n.mueller@gmail.com>
03bdc86
to
613e279
Compare
Ok, let's get this in ;) |
`using Async.Spawn` by itself does *not* obey the normal Scoping rules for Async contexts. See lampepfl/gears#46.
What's this?
The goal is to disallow spawning dangling Futures from
using Async
functions.Async.Spawn
is an opaque alias ofAsync
, defined as a subtype ofAsync
,obtained by explicitly "upgrading" it through
Async.group
-- or automatically giventhrough
Async.blocking
orFuture.apply
.The
Async.Spawn
-taking functions (signalling usage of dangling futures)should follow the hacky signature of
Future.apply
:to ensure that the given
Async
instance (which is usually synthesizedto be the innermost context) is the same instance as the
Async.Spawn
instance. It happens quite often (especially when nesting
Async
contexts)that these don't match:
with the
Future.apply
signature as above, this does not happen and willgive a compile time error.