Skip to content
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 RunOnMainThread to take control of the main thread #64777

Open
eliasnaur opened this issue Dec 17, 2023 · 5 comments
Open
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. Proposal
Milestone

Comments

@eliasnaur
Copy link
Contributor

eliasnaur commented Dec 17, 2023

Proposal Details

This proposal is a less flexible but perhaps easier to implement and maintain alternative to #64755. See that issue for background and motivation for non-main packages to take control of the main thread.

I propose adding a new function, runtime.RunOnMainThread, for running a function on the startup, or main, thread. In particular,

package runtime

// RunOnMainThread runs a function immediately after program initialization on a
// goroutine wired to the startup thread. It panics if called outside an init
// function, if called more than once, or if [runtime.LockOSThread] has already
// been called from an init function.
// Once RunOnMainThread is called, later LockOSThread calls from an init function
// will panic.
func RunOnMainThread(f func())

This is the complete proposal.

Variants

Just like #64755, an alternative spelling is syscall.RunOnMainThread optionally limited to GOOS=darwin and GOOS=ios.

@aclements
Copy link
Member

The combination of "It panics if called outside an init function" and "[it panics] if called more than once" is rather unfortunate. If a project happen to (perhaps transitively) pull in two packages that both might need main thread functionality, but the project doesn't actually need that functionality from both packages, they're now in a pickle because the mere act of importing the package must run init functions, and those must call RunOnMainThread if there's any chance the package may need to use the main thread.

@eliasnaur
Copy link
Contributor Author

eliasnaur commented Dec 18, 2023

Here's a neat variant that you may like better:

// RunOnOSThread runs the function in a new goroutine
// wired to the thread of the caller. If the caller has locked
// the thread with `LockOSThread`, it is unlocked before
// returning. The caller continues on a different thread.
func RunOnOSThread(f func())

The advantages are:

  • No panics nor blocking.
  • Works any time for any thread, not just the main.
  • Complements LockOSThread.
  • Composable: multiple callers don't interfere with each other.
  • Easier to implement than LockMainOSThread (I believe). In particular, RunOnOSThread performs a context switch similar to what the runtime will do to make iterators efficient.

The disadvantages are:

  • Explicitly starting a goroutine is a weird API, but contrary to the original proposal, the function is executed immediately.
  • Calling RunOnOSThread during init switches the remaining initialization to a different thread. If this is unacceptable, we could say that
// RunOnOSThread panics if run from an init function.

at the cost of a panic condition and forcing main thread APIs to require some call from the main goroutine. E.g.

package app // gioui.org/app

// Window represent a platform GUI window.
// Because of platform limitations, at least once one of Window's
// methods must be called from the main goroutine.
// Otherwise, call [Main].
type Window struct {...}

// Main must be called from the main goroutine at least once,
// if no [Window] methods will be called from the main goroutine.
func Main()

@eliasnaur eliasnaur added the compiler/runtime Issues related to the Go compiler and/or runtime. label Jan 20, 2024
@eliasnaur
Copy link
Contributor Author

Gentle ping. I believe #64777 (comment) addresses the objections @aclements brought up here and on #64755.

@ianlancetaylor
Copy link
Contributor

As I understand it the goal here is for an imported package to be able to run non-Go code on the initial program thread. The RunOnOSThread function variant is kind of useless if run on anything other than the initial program thread; if you don't care that thread you are on, it could be replaced by a go statement that starts by calling runtime.LockOSThread. So RunOnOSThread seems like a confusing way to accomplish the goal.

Honestly it does not seem so terrible to me if an operation that has to take place on the initial program thread requires some cooperation from the main function. I understand that that is a bit annoying. But it's a bit annoying for frameworks to require running on the initial program thread.

@eliasnaur
Copy link
Contributor Author

As I understand it the goal here is for an imported package to be able to run non-Go code on the initial program thread. The RunOnOSThread function variant is kind of useless if run on anything other than the initial program thread; if you don't care that thread you are on, it could be replaced by a go statement that starts by calling runtime.LockOSThread. So RunOnOSThread seems like a confusing way to accomplish the goal.

RunOnOSThread tries to juggle several goals: orthogonality, few special cases, easy to describe, implement and maintain. I think it achieves those goals fairly well, but I understand if you think it's too general for its special purpose. Would placing it in package cgo, syscall, or x/sys make a difference? Would spelling it RunOnMainThread and panicing if called without the main thread wired?

Honestly it does not seem so terrible to me if an operation that has to take place on the initial program thread requires some cooperation from the main function. I understand that that is a bit annoying.

The number of direct uses of RunOnOSThread is probably low, say a handful of distinct cases. But please consider the larger indirect impact: every GUI program and Windows service written in Go (at least).

The alternative is not entirely trivial either. Consider a straightforward CLI tool that you want to add an optional GUI to, or a CLI service you optionally want to run as a Windows service. You then have to change your program flow, e.g. rewriting your logic as an interface with callbacks, and you must call the API from your main function. Both requirements are alien to Go programmers not familiar with the underlying frameworks.

Case in point, it took me a while to figure out why svc.Run didn't work from a goroutine and I'm used to macOS main thread APIs. Further, the changes to turn my otherwise straightforward http.ListenAndServe program to conform to svc.Handler were frankly obnoxious and felt disproportional to the effect.

But it's a bit annoying for frameworks to require running on the initial program thread.

So let's not pass on the annoyance to Go programmers :-)

Go often makes quality-of-life changes that are not strictly necessary, sometimes at non-trivial cost: cgo.Handle, replacing // +build with the more intuitive //go:build, runtime.Pinner come to mind. I hope RunOnOSThread (or something with similar effect) can be another such change, delighting programmers by making their programs simpler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. Proposal
Projects
Status: No status
Status: Incoming
Development

No branches or pull requests

5 participants
@eliasnaur @aclements @ianlancetaylor @gopherbot and others