-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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
proposal: runtime: add LockMainOSThread to take control of the main thread #64755
Comments
If I understand correctly, this approach is working today:
Would an alternative be to add In other words, it isn't quite clear to me what the concrete benefit of What happens if two different goroutines call cc @golang/runtime |
It works in the sense that every user of, say, import "gioui.org/app"
func main() {
go func() {
w := new(app.Window)
for {
e := w.NextEvent()
// Respond to e, usually by redrawing window content.
}
}()
app.Main()
} and import "gioui.org/app"
func main() {
w := new(app.Window)
for {
e := w.NextEvent()
// Respond to e, usually by redrawing window content.
}
}
This is already documented as a guarantee by
Yes.
One (or both) block, waiting for the main thread to become available. |
I've filed an less flexible but perhaps simpler alternative: #64777. Both proposals now also mention the |
Anecdotally, and without bearing on the proposal, I just want to mention that Go is unique to be able to offer this feature by virtue of goroutines. Other GUI libraries either revert to a callback design or live with the hacky workarounds. For example, a prominent Rust library first considered using user mode context switching (similar to how goroutines can switch OS thread), but eventually ended up inverting their control flow to a callback design. |
I actually prefer the principle of this proposal over the callback alternative in #64777. Part of the philosophy of goroutines is that you should be able to write straight-line (blocking) code and the language implementation will hide the details of context management and control flow. #64777 feels more akin to async/promise-style programming. That said, LockMainOSThread certainly has some issues:
I see you've spelled out some clever restrictions on #64777's RunOnMainThread that work around these issues, but I also feel like they do so by exposing implementation limitations and leaning into non-composability. :) |
It's hopeless to design for more than one package being able to use the main thread, without explicit coordination. In fact, my motivating use-case is a native call that never returns. That leaves the behaviour when more than one goroutine calls LockMainOSThread. I'm proposing the loser block, which as you point out leads to a likely deadlock. How about panicing, similar to the "deadlock, all goroutines are asleep" condition?
Assuming a losing LockMainOSThread panics, I imagine implementation becomes easier because a goroutine will never be parked with in a special state (e.g. waitingForMainThread). Something like
This implementation requires the Go runtime to never schedule anything on the main thread, thus wasting a thread in the general case. I don't know the complexity of forcing a (non-syscalling) goroutine off a thread. I'm no runtime export, so I apologize if the above is hopelessly naïve. |
Goroutines preempt one specific thread ^ It looks much simpler/clear to keep things in |
Another use-case came to my attention: because Windows services require the main thread, every Go package for running a Go program as a Windows service expose contorted callback APIs. Examples: https://pkg.go.dev/golang.org/x/sys/windows/svc#Run, https://pkg.go.dev/github.com/kardianos/service#Service (see its Run documentation), https://pkg.go.dev/github.com/judwhite/go-svc#Run. In fact, none of the various I believe with this proposal, or a variant, you'll be able to expose a Windows service with a straightforward, non-blocking func main() {
svc.SetupService()
// do rest of setup: open database connections, start HTTP listener etc.
} Or, if you're interested in service events: func main() {
go func() {
evts, err := svc.RunService()
for {
e, ok := evts.Event()
// handle and respond to e.
}
}()
// do rest of setup: open database connections, start HTTP listener etc.
} |
How would several libraries utilizing this functionality coordinate? In the current state ( The only combination that comes to mind immediately is a debugger under Darwin, where both
|
Like today: explicitly.
Not so for AppKit, UIKit nor Windows services: they all require the main thread and never relinquish control of it. The best you can do is to ask nicely for a callback. This proposal is primarily for enabling Go packages to expose natural Go APIs for main-thread greedy platform facilities.
@dottedmag you may like the #64777 (comment) variant better. It's currently my favorite. |
Despite the option to control the main thread event loop, some gestures still block the event loop until completion. This change disables the blocking resize gestures and implements a non-blocking replacement. A complete replacement is left for future work, or the implementation of golang/go#64755. Signed-off-by: Elias Naur <[email protected]>
Despite the option to control the main thread event loop, some gestures still block the event loop until completion. This change disables the blocking resize gestures and implements a non-blocking replacement. A complete replacement is left for future work, or the implementation of golang/go#64755. Signed-off-by: Elias Naur <[email protected]>
Despite the option to control the main thread event loop, some gestures still block the event loop until completion. This change disables the blocking resize gestures and implements a non-blocking replacement. A complete replacement is left for future work, or the implementation of golang/go#64755. Signed-off-by: Elias Naur <[email protected]>
Despite the option to control the main thread event loop, some gestures still block the event loop until completion. This change disables the blocking resize gestures and implements a non-blocking replacement. A complete replacement is left for future work, or the implementation of golang/go#64755. Signed-off-by: Elias Naur <[email protected]>
Retracted in favor of #67694. See also rationale and workarounds described in #64777 (comment). |
Proposal Details
I propose adding a new function,
runtime.LockMainOSThread
, which is likeLockOSThread
but for the "main" (or "startup") thread. In particular,This is the complete proposal.
Variants
If the
runtime
package is deemed too unpalatable, one variant is to spell the new functionsyscall.LockMainOSThread
and optionally limit it toGOOS=darwin
andGOOS=ios
.The #64777 alternative is less flexible, but perhaps easier to maintain.
Background
Some APIs, most notably macOS'
AppKit
and iOS'UIKit
require exclusive control of the startup thread. Go already supports such APIs by specifying that the main function runs on the main thread ifruntime.LockOSThread
is called in an init function. Unfortunately, Go's design doesn't allow a non-main package to take control of the main thread, and so any use of main thread APIs bleed through to the user.For example, this is Gio's current API for creating and driving a GUI window:
Note the extra goroutine and
app.Main
call.This proposal allows a non-main package to take control of the main thread, hiding that implementation detail from the user.
For example, package
gioui.org/app
has aninit
function that callsruntime.LockOSThread
to guarantee the main thread requirement. With this proposal, thegioui.org/app
package could replace thatinit
function,with a lazy initializer:
and allow the user-facing API to be reduced to the straightforward
Alternatives
Some alternatives are:
[NSApp run]
to return and drive the main thread event loop from Go. However, this option has several drawbacks:go run
andgo build
of self-contained executables. With this proposal, it's feasible to implementgo build
(and evengo -exec ... run
) for iOS.Backwards compatibility
This proposal is backwards compatible.
The text was updated successfully, but these errors were encountered: