-
Notifications
You must be signed in to change notification settings - Fork 18k
iter: iter.Pull
should forward panics and runtime.Goexit
[freeze exception]
#67712
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
Comments
cc @golang/compiler |
We've gone back and forth on whether it should be documented in terms of goroutine+channel semantics (that is, I think maybe what does have to be documented is that panics do not propagate through an |
IIUC, the actual implementation is a coroutine rather than a regular goroutine. If the semantics is the same as goroutine, I was wondering why a coroutine is used instead of a regular goroutine. Is this for performance? If so, now we have issues around Cgo with iter.Pull (#67499), isn't this a kind of early optimization? Or, might the semantics be changed in the future? |
It's really a fast, direct switch between goroutines that doesn't go through the scheduler. This is functionally similar to a coroutine switch, hence why it's called
The semantics are explicitly not the same as a goroutine. The semantics are a little less well-defined than that at the moment. Also, even if we formalized the notion of coroutines in the language, I'm not sure I agree that a panic should be able to cross coroutine boundaries.
Yes, it's for performance. The direct switch is very, very cheap compared to implementing the same thing with channels. I get what you're saying about the optimization possibly happening too early (this was discussed at length), but the performance of |
Are there many languages where an exception in a coroutine isn't propagated? |
An exception in a coroutine is caught in Ruby and JavaScript: |
Oh, that's interesting... At least in JS it really makes sense that exceptions would propagate through. On the caller side (and the definition side!) it just looks almost like calling a regular function, especially with the syntax. I think what's tricky about Go is that it's already possible for functions to do more complicated things with a closure (like sending it to another goroutine) inside of a function call, and this is a somewhat normal thing to do. That being said, I do think you changed my mind about:
I think there's a good argument to be made about that. However, for |
A goroutine implementation could capture the panic and send it to the other side to be re-panic'd when next/close get invoked. https://go.dev/play/p/ltXl_x_CPvm |
@jimmyfrasche That's true, that you can re-panic, but what I meant by propagation is that the original panic stack follows (for example, if the panic is uncaught, you lose the context by re-panicking). But maybe that doesn't matter? The JS example at least doesn't seem to provide any context on an uncaught exception (though that alone isn't a reason to replicate the behavior). I think I'm personally still leaning toward the panic not propagating just because that seems more consistent within the language. Is there any existing API that runs a function on another goroutine and possibly re-panics like this? Coroutines don't currently exist in the language, so it would be a little weird to say |
After discussing offline with @aclements, I think I'm convinced that we should propagate panics. We can start out by just re-panicking: the user-visible behavior is the most important part for this release cycle. Here's the information that changed how I was leaning. Although the primary example that came to mind (
Furthermore, we wanted to leave the door open to a compiler-driven CPS transform as a valid implementation of an And this is a situation of "if we can, we should," and we definitely "can," since the implementation controls the creation of the I will send a CL later to forward panics. |
iter.Pull
is not recoverediter.Pull
should forward panics
I've retitled this issue to focus on the API question rather than the implementation. The core API question is what should happen if the sequence function passed to iter.Pull panics. Regardless of the mechanics of iter.Pull, we have two options: passing it through to the caller of iter.Pull, or crashing the program. Whether its implemented with goroutines+channels, coroutines, or CPS transform doesn't matter, since any of those implementations can support either semantics (though obviously which is more "natural" varies). I'm also leaning in favor of passing the panic through. I worry about making changes like this so late in the release, but I think this will also be a simple change. I think once @mknyszek has prepared a CL, we'll have to assess its risk and weigh requesting a freeze exception. Passing the panic through is consistent with the APIs @mknyszek pointed out above. I think it's also more consistent with where we landed on panic handling of for-range loops. Supposing you have a for x := range Zip(a, b) { ... } It seems unfortunate that a panic in |
I believe I intended panics to forward: https://research.swtch.com/coro#panic. |
iter.Pull
should forward panicsiter.Pull
should forward panics [freeze exception]
I have an implementation that propagates panics, and also converts a Also, the implementation is pretty simple, so I think it would be OK to land during the freeze, especially since nobody is using https://go.dev/cl/589136 and https://go.dev/cl/589137 are the CLs. |
Change https://go.dev/cl/589137 mentions this issue: |
Change https://go.dev/cl/589136 mentions this issue: |
I'm requesting a freeze exception for this issue. See my previous comment for a rationale. CC @golang/release |
Thanks! Woudn't stack trace info be continuous? |
My inclination would be to turn Goexit from the iterator into a Goexit on the caller. That's sort of "maximum" transparency to the implementation mechanism and most symmetric in my zip example. |
I'm not positive I understand your question, but with that implementation it's true that the stack trace from (Just recording my thoughts on this: I think we'd do this by having the panic unwinder know that it needs to hop goroutines when it leaves the Pull goroutine, and leave the Pull goroutine around until the panic ends. After that hop, any defers would have to run on the "parent" goroutine, and traceback from the defer would have to understand to jump to the Pull goroutine once it got out of the defers, and then back over to the parent goroutine when it reaches the end of the Pull goroutine. I believe this can be arbitrarily nested. This all sounds pretty complicated, but I think it may not be too bad if we have a mechanism for marking the frames that have these special goroutine jumps.) |
Yeah that's what I wanted to know, thanks |
I updated the changes to propagate |
Change https://go.dev/cl/590075 mentions this issue: |
Here is a summary of the semantics we plan to set for
These are the semantics I have implemented in CL 590075, CL 589136, and CL 589137 respectively. Note: a Motivating examples:
|
iter.Pull
should forward panics [freeze exception]iter.Pull
should forward panics and runtime.Goexit
[freeze exception]
We reviewed this in a release meeting, and agreed to approve its "freeze exception" bit. Thanks for letting us know. |
Discussed in proposal review. We decided that since forwarding panics was intended to be part of the behavior of |
iter.Pull
should forward panics and runtime.Goexit
[freeze exception]iter.Pull
should forward panics and runtime.Goexit
[freeze exception]
This change propagates panics from the iterator passed to Pull through next and stop. Once the panic occurs, next and stop become no-ops (the iterator is invalidated). For #67712. Change-Id: I05e45601d4d10acdf51b53e3164bd891c1b324ac Reviewed-on: https://go-review.googlesource.com/c/go/+/589136 Reviewed-by: David Chase <drchase@google.com> Reviewed-by: Austin Clements <austin@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Consider the following code snippet: next, stop := iter.Pull(seq) stop() Today, seq will iterate exactly once before it notices that its iteration is invalid to begin with. This effect is observable in a variety of ways. For example, if the iterator panics, since that panic must propagate to the caller of stop. But if the iterator is stateful in anyway, then it may update some state. This is somewhat unexpected and because it's observable, can be depended upon. This behavior does not align well with other possible implementations of Pull, like CPS performed by the compiler. It's also just odd to let even one iteration happen, precisely because of unexpected state modification. Fix this by not iterating at all of the done flag is set before entering the iterator. For #67712. Change-Id: I18162e29df45a2e8968f68379450d92e1de47c4d Reviewed-on: https://go-review.googlesource.com/c/go/+/590075 Reviewed-by: David Chase <drchase@google.com> Reviewed-by: Austin Clements <austin@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Go version
devel go1.23-a3a584e4ab Wed May 29 13:52:34 2024 +0000
Output of
go env
in your module/workspace:What did you do?
Run this on the playground
https://go.dev/play/p/_BjecTfmPE8?v=gotip
What did you see happen?
The panic is not caught at
recover()
What did you expect to see?
The panic is caught at
recover()
The text was updated successfully, but these errors were encountered: